-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #929 from hashicorp/sebasslash/main
feat: add support for run task results callback
- Loading branch information
Showing
8 changed files
with
259 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package tfe | ||
|
||
import ( | ||
"time" | ||
) | ||
|
||
// RunTaskRequest is the payload object that TFC/E sends to the Run Task's URL. | ||
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#common-properties | ||
type RunTaskRequest struct { | ||
AccessToken string `json:"access_token"` | ||
Capabilitites RunTaskRequestCapabilitites `json:"capabilitites,omitempty"` | ||
ConfigurationVersionDownloadURL string `json:"configuration_version_download_url,omitempty"` | ||
ConfigurationVersionID string `json:"configuration_version_id,omitempty"` | ||
IsSpeculative bool `json:"is_speculative"` | ||
OrganizationName string `json:"organization_name"` | ||
PayloadVersion int `json:"payload_version"` | ||
PlanJSONAPIURL string `json:"plan_json_api_url,omitempty"` // Specific to post_plan, pre_apply or post_apply stage | ||
RunAppURL string `json:"run_app_url"` | ||
RunCreatedAt time.Time `json:"run_created_at"` | ||
RunCreatedBy string `json:"run_created_by"` | ||
RunID string `json:"run_id"` | ||
RunMessage string `json:"run_message"` | ||
Stage string `json:"stage"` | ||
TaskResultCallbackURL string `json:"task_result_callback_url"` | ||
TaskResultEnforcementLevel string `json:"task_result_enforcement_level"` | ||
TaskResultID string `json:"task_result_id"` | ||
VcsBranch string `json:"vcs_branch,omitempty"` | ||
VcsCommitURL string `json:"vcs_commit_url,omitempty"` | ||
VcsPullRequestURL string `json:"vcs_pull_request_url,omitempty"` | ||
VcsRepoURL string `json:"vcs_repo_url,omitempty"` | ||
WorkspaceAppURL string `json:"workspace_app_url"` | ||
WorkspaceID string `json:"workspace_id"` | ||
WorkspaceName string `json:"workspace_name"` | ||
WorkspaceWorkingDirectory string `json:"workspace_working_directory,omitempty"` | ||
} | ||
|
||
// RunTaskRequestCapabilitites defines the capabilities that the caller supports. | ||
type RunTaskRequestCapabilitites struct { | ||
Outcomes bool `json:"outcomes"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package tfe | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
) | ||
|
||
// Compile-time proof of interface implementation. | ||
var _ RunTasksIntegration = (*runTaskIntegration)(nil) | ||
|
||
// RunTasksIntegration describes all the Run Tasks Integration Callback API methods. | ||
// | ||
// TFE API docs: | ||
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration | ||
type RunTasksIntegration interface { | ||
// Update sends updates to TFC/E Run Task Callback URL | ||
Callback(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error | ||
} | ||
|
||
// taskResultsCallback implements RunTasksIntegration. | ||
type runTaskIntegration struct { | ||
client *Client | ||
} | ||
|
||
// TaskResultCallbackRequestOptions represents the TFC/E Task result callback request | ||
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1 | ||
type TaskResultCallbackRequestOptions struct { | ||
Type string `jsonapi:"primary,task-results"` | ||
Status TaskResultStatus `jsonapi:"attr,status"` | ||
Message string `jsonapi:"attr,message,omitempty"` | ||
URL string `jsonapi:"attr,url,omitempty"` | ||
Outcomes []*TaskResultOutcome `jsonapi:"relation,outcomes,omitempty"` | ||
} | ||
|
||
// TaskResultOutcome represents a detailed TFC/E run task outcome, which improves result visibility and content in the TFC/E UI. | ||
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#outcomes-payload-body | ||
type TaskResultOutcome struct { | ||
Type string `jsonapi:"primary,task-result-outcomes"` | ||
OutcomeID string `jsonapi:"attr,outcome-id,omitempty"` | ||
Description string `jsonapi:"attr,description,omitempty"` | ||
Body string `jsonapi:"attr,body,omitempty"` | ||
URL string `jsonapi:"attr,url,omitempty"` | ||
Tags map[string][]*TaskResultTag `jsonapi:"attr,tags,omitempty"` | ||
} | ||
|
||
// TaskResultTag can be used to enrich outcomes display list in TFC/E. | ||
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#severity-and-status-tags | ||
type TaskResultTag struct { | ||
Label string `json:"label"` | ||
Level string `json:"level,omitempty"` | ||
} | ||
|
||
// Update sends updates to TFC/E Run Task Callback URL | ||
func (s *runTaskIntegration) Callback(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error { | ||
if !validString(&callbackURL) { | ||
return ErrInvalidCallbackURL | ||
} | ||
if !validString(&accessToken) { | ||
return ErrInvalidAccessToken | ||
} | ||
if err := options.valid(); err != nil { | ||
return err | ||
} | ||
req, err := s.client.NewRequest(http.MethodPatch, callbackURL, &options) | ||
if err != nil { | ||
return err | ||
} | ||
// The PATCH request must use the token supplied in the originating request (access_token) for authentication. | ||
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-headers-1 | ||
req.Header.Set("Authorization", "Bearer "+accessToken) | ||
return req.Do(ctx, nil) | ||
} | ||
|
||
func (o *TaskResultCallbackRequestOptions) valid() error { | ||
if o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning { | ||
return ErrInvalidTaskResultsCallbackStatus | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package tfe | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// TestRunTasksIntegration_Validate runs a series of tests that test whether various TaskResultCallbackRequestOptions objects can be considered valid or not | ||
func TestRunTasksIntegration_Validate(t *testing.T) { | ||
t.Run("with an empty status", func(t *testing.T) { | ||
opts := TaskResultCallbackRequestOptions{Status: ""} | ||
err := opts.valid() | ||
assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error()) | ||
}) | ||
t.Run("without valid Status options", func(t *testing.T) { | ||
for _, s := range []TaskResultStatus{TaskPending, TaskErrored, "foo"} { | ||
opts := TaskResultCallbackRequestOptions{Status: s} | ||
err := opts.valid() | ||
assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error()) | ||
} | ||
}) | ||
t.Run("with valid Status options", func(t *testing.T) { | ||
for _, s := range []TaskResultStatus{TaskFailed, TaskPassed, TaskRunning} { | ||
opts := TaskResultCallbackRequestOptions{Status: s} | ||
err := opts.valid() | ||
require.NoError(t, err) | ||
} | ||
}) | ||
} | ||
|
||
// TestTaskResultsCallbackRequestOptions_Marshal tests whether you can properly serialise a TaskResultCallbackRequestOptions object | ||
// You may find the expected body here: https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1 | ||
func TestTaskResultsCallbackRequestOptions_Marshal(t *testing.T) { | ||
opts := TaskResultCallbackRequestOptions{ | ||
Status: TaskPassed, | ||
Message: "4 passed, 0 skipped, 0 failed", | ||
URL: "https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ", | ||
Outcomes: []*TaskResultOutcome{ | ||
{ | ||
OutcomeID: "PRTNR-CC-TF-127", | ||
Description: "ST-2942:S3 Bucket will not enforce MFA login on delete requests", | ||
Body: "# Resolution for issue ST-2942\n\n## Impact\n\nFollow instructions in the [AWS S3 docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html) to manually configure the MFA setting.\n—-- Payload truncated —--", | ||
URL: "https://external.service.dev/result/PRTNR-CC-TF-127", | ||
Tags: map[string][]*TaskResultTag{ | ||
"Status": {&TaskResultTag{Label: "Denied", Level: "error"}}, | ||
"Severity": { | ||
&TaskResultTag{Label: "High", Level: "error"}, | ||
&TaskResultTag{Label: "Recoverable", Level: "info"}, | ||
}, | ||
"Cost Centre": {&TaskResultTag{Label: "IT-OPS"}}, | ||
}, | ||
}, | ||
}, | ||
} | ||
require.NoError(t, opts.valid()) | ||
reqBody, err := serializeRequestBody(&opts) | ||
require.NoError(t, err) | ||
expectedBody := `{"data":{"type":"task-results","attributes":{"message":"4 passed, 0 skipped, 0 failed","status":"passed","url":"https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ"},"relationships":{"outcomes":{"data":[{"type":"task-result-outcomes","attributes":{"body":"# Resolution for issue ST-2942\n\n## Impact\n\nFollow instructions in the [AWS S3 docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html) to manually configure the MFA setting.\n—-- Payload truncated —--","description":"ST-2942:S3 Bucket will not enforce MFA login on delete requests","outcome-id":"PRTNR-CC-TF-127","tags":{"Cost Centre":[{"label":"IT-OPS"}],"Severity":[{"label":"High","level":"error"},{"label":"Recoverable","level":"info"}],"Status":[{"label":"Denied","level":"error"}]},"url":"https://external.service.dev/result/PRTNR-CC-TF-127"}}]}}}} | ||
` | ||
assert.Equal(t, reqBody.(*bytes.Buffer).String(), expectedBody) | ||
} | ||
|
||
func TestRunTasksIntegration_ValidateCallback(t *testing.T) { | ||
t.Run("with invalid callbackURL", func(t *testing.T) { | ||
trc := runTaskIntegration{client: nil} | ||
err := trc.Callback(context.Background(), "", "", TaskResultCallbackRequestOptions{}) | ||
assert.EqualError(t, err, ErrInvalidCallbackURL.Error()) | ||
}) | ||
t.Run("with invalid accessToken", func(t *testing.T) { | ||
trc := runTaskIntegration{client: nil} | ||
err := trc.Callback(context.Background(), "https://app.terraform.io/foo", "", TaskResultCallbackRequestOptions{}) | ||
assert.EqualError(t, err, ErrInvalidAccessToken.Error()) | ||
}) | ||
} | ||
|
||
func TestRunTasksIntegration_Callback(t *testing.T) { | ||
ts := runTaskCallbackMockServer(t) | ||
defer ts.Close() | ||
|
||
client, err := NewClient(&Config{ | ||
RetryServerErrors: true, | ||
Token: testInitialClientToken, | ||
Address: ts.URL, | ||
}) | ||
require.NoError(t, err) | ||
trc := runTaskIntegration{ | ||
client: client, | ||
} | ||
req := RunTaskRequest{ | ||
AccessToken: testTaskResultCallbackToken, | ||
TaskResultCallbackURL: ts.URL, | ||
} | ||
err = trc.Callback(context.Background(), req.TaskResultCallbackURL, req.AccessToken, TaskResultCallbackRequestOptions{Status: TaskPassed}) | ||
require.NoError(t, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters