Skip to content

Commit

Permalink
Merge pull request #570 from hashicorp/aw/force-execute-run
Browse files Browse the repository at this point in the history
Add force execute for a run
  • Loading branch information
annawinkler committed Nov 1, 2022
2 parents c435011 + d504b98 commit 6777fea
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 0 deletions.
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

0 comments on commit 6777fea

Please sign in to comment.