Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add force execute for a run #570

Merged
merged 1 commit into from Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
* Add `NotificationTriggerAssessmentCheckFailed` notification trigger type by @rexredinger [#549](https://github.com/hashicorp/go-tfe/pull/549)
* Add `RemoteTFEVersion()` to the `Client` interface, which exposes the `X-TFE-Version` header set by a remote TFE instance by @sebasslash [#563](https://github.com/hashicorp/go-tfe/pull/563)
* Validate the module version as a version instead of an ID [#409](https://github.com/hashicorp/go-tfe/pull/409)
* Add `ForceExecute()` to `Runs` to allow force executing a run by @annawinkler [#570](https://github.com/hashicorp/go-tfe/pull/570)

# v1.11.0

Expand Down
14 changes: 14 additions & 0 deletions mocks/run_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions run.go
Expand Up @@ -36,6 +36,9 @@ type Runs interface {
// Force-cancel a run by its ID.
ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error

// Force execute a run by its ID.
ForceExecute(ctx context.Context, runID string) error

// Discard a run by its ID.
Discard(ctx context.Context, runID string, options RunDiscardOptions) error
}
Expand Down Expand Up @@ -464,6 +467,26 @@ func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCa
return req.Do(ctx, nil)
}

// ForceExecute is used to forcefully execute a run by its ID.
//
// Note: While useful at times, force-executing a run circumvents the typical
// workflow of applying runs using Terraform Cloud. It is not intended for
// regular use. If you find yourself using it frequently, please reach out to
// HashiCorp Support for help in developing an alternative approach.
func (s *runs) ForceExecute(ctx context.Context, runID string) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}

u := fmt.Sprintf("runs/%s/actions/force-execute", url.QueryEscape(runID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}

return req.Do(ctx, nil)
}

// Discard a run by its ID.
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
if !validStringID(&runID) {
Expand Down
57 changes: 57 additions & 0 deletions run_integration_test.go
Expand Up @@ -526,6 +526,63 @@ func TestRunsForceCancel(t *testing.T) {
})
}

func TestRunsForceExecute(t *testing.T) {
skipIfNotCINode(t)

client := testClient(t)
ctx := context.Background()

wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()

// We need to create 2 runs here:
// - The first run will automatically be planned so that the second
// run can't be executed.
// - The second run will be pending until the first run is confirmed or
// discarded, so we will force execute this run.
rToCancel, _ := createPlannedRun(t, client, wTest)
rTest, _ := createRunWaitForStatus(t, client, wTest, RunPending)

t.Run("a successful force-execute", func(t *testing.T) {
// Verify the user has permission to force-execute the run
assert.True(t, rTest.Permissions.CanForceExecute)

err := client.Runs.ForceExecute(ctx, rTest.ID)
require.NoError(t, err)

timeout := 2 * time.Minute
ctxPollRunForceExecute, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

// Verify the second run has a status that is an applyable status
rTest = pollRunStatus(t,
client,
ctxPollRunForceExecute,
rTest,
applyableStatuses(rTest))
if rTest.Status == RunErrored {
fatalDumpRunLog(t, client, ctx, rTest)
}

// Refresh the view of the first run
rToCancel, err = client.Runs.Read(ctx, rToCancel.ID)
require.NoError(t, err)

// Verify the first run was discarded
assert.Equal(t, RunDiscarded, rToCancel.Status)
})

t.Run("when the run does not exist", func(t *testing.T) {
err := client.Runs.ForceExecute(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("with invalid run ID", func(t *testing.T) {
err := client.Runs.ForceExecute(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}

func TestRunsDiscard(t *testing.T) {
skipIfNotCINode(t)

Expand Down