Emit warnings for certain run events in cloud backend (#33020)

The cloud backend, which communicates with TFC like APIs, can create
runs which may have one more configuration parameters altered. These
alterations are emitted as run-events on the run so that API clients
can consume and display them to users. This commit adds a step in
plan operation to query the run-events once a run is created and then
emit specific run-event descriptions to the console as warnings for
the user.
This commit is contained in:
Glenn Sarti 2023-04-17 23:53:47 +08:00 committed by GitHub
parent c10a07f3b2
commit 7e2e834aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 234 additions and 0 deletions

View File

@ -144,6 +144,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) {
b.client.Plans = mc.Plans
b.client.PolicyChecks = mc.PolicyChecks
b.client.Runs = mc.Runs
b.client.RunEvents = mc.RunEvents
b.client.StateVersions = mc.StateVersions
b.client.Variables = mc.Variables
b.client.Workspaces = mc.Workspaces

View File

@ -294,6 +294,11 @@ in order to capture the filesystem context the remote workspace expects:
runHeader, b.hostname, b.organization, op.Workspace, r.ID)) + "\n"))
}
// Render any warnings that were raised during run creation
if err := b.renderRunWarnings(stopCtx, b.client, r.ID); err != nil {
return r, err
}
// Retrieve the run to get task stages.
// Task Stages are calculated upfront so we only need to call this once for the run.
taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID)

View File

@ -0,0 +1,46 @@
package cloud
import (
"context"
"fmt"
"strings"
tfe "github.com/hashicorp/go-tfe"
)
const (
changedPolicyEnforcementAction = "changed_policy_enforcements"
changedTaskEnforcementAction = "changed_task_enforcements"
ignoredPolicySetAction = "ignored_policy_sets"
)
func (b *Cloud) renderRunWarnings(ctx context.Context, client *tfe.Client, runId string) error {
if b.CLI == nil {
return nil
}
result, err := client.RunEvents.List(ctx, runId, nil)
if err != nil {
return err
}
if result == nil {
return nil
}
// We don't have to worry about paging as the API doesn't support it yet
for _, re := range result.Items {
switch re.Action {
case changedPolicyEnforcementAction, changedTaskEnforcementAction, ignoredPolicySetAction:
if re.Description != "" {
b.CLI.Warn(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf(
runWarningHeader, re.Description)) + "\n"))
}
}
}
return nil
}
const runWarningHeader = `
[reset][yellow]Warning:[reset] %s
`

View File

@ -0,0 +1,153 @@
package cloud
import (
"context"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/hashicorp/go-tfe"
tfemocks "github.com/hashicorp/go-tfe/mocks"
"github.com/mitchellh/cli"
)
func MockAllRunEvents(t *testing.T, client *tfe.Client) (fullRunID string, emptyRunID string) {
ctrl := gomock.NewController(t)
fullRunID = "run-full"
emptyRunID = "run-empty"
mockRunEventsAPI := tfemocks.NewMockRunEvents(ctrl)
emptyList := tfe.RunEventList{
Items: []*tfe.RunEvent{},
}
fullList := tfe.RunEventList{
Items: []*tfe.RunEvent{
{
Action: "created",
CreatedAt: time.Now(),
Description: "",
},
{
Action: "changed_task_enforcements",
CreatedAt: time.Now(),
Description: "The enforcement level for task 'MockTask' was changed to 'advisory' because the run task limit was exceeded.",
},
{
Action: "changed_policy_enforcements",
CreatedAt: time.Now(),
Description: "The enforcement level for policy 'MockPolicy' was changed to 'advisory' because the policy limit was exceeded.",
},
{
Action: "ignored_policy_sets",
CreatedAt: time.Now(),
Description: "The policy set 'MockPolicySet' was ignored because the versioned policy set limit was exceeded.",
},
{
Action: "queued",
CreatedAt: time.Now(),
Description: "",
},
},
}
// Mock Full Request
mockRunEventsAPI.
EXPECT().
List(gomock.Any(), fullRunID, gomock.Any()).
Return(&fullList, nil).
AnyTimes()
// Mock Full Request
mockRunEventsAPI.
EXPECT().
List(gomock.Any(), emptyRunID, gomock.Any()).
Return(&emptyList, nil).
AnyTimes()
// Mock a bad Read response
mockRunEventsAPI.
EXPECT().
List(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, tfe.ErrInvalidRunID).
AnyTimes()
// Wire up the mock interfaces
client.RunEvents = mockRunEventsAPI
return
}
func TestRunEventWarningsAll(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
config := &tfe.Config{
Token: "not-a-token",
}
client, _ := tfe.NewClient(config)
fullRunID, _ := MockAllRunEvents(t, client)
ctx := context.TODO()
err := b.renderRunWarnings(ctx, client, fullRunID)
if err != nil {
t.Fatalf("Expected to not error but received %s", err)
}
output := b.CLI.(*cli.MockUi).ErrorWriter.String()
testString := "The enforcement level for task 'MockTask'"
if !strings.Contains(output, testString) {
t.Fatalf("Expected %q to contain %q but it did not", output, testString)
}
testString = "The enforcement level for policy 'MockPolicy'"
if !strings.Contains(output, testString) {
t.Fatalf("Expected %q to contain %q but it did not", output, testString)
}
testString = "The policy set 'MockPolicySet'"
if !strings.Contains(output, testString) {
t.Fatalf("Expected %q to contain %q but it did not", output, testString)
}
}
func TestRunEventWarningsEmpty(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
config := &tfe.Config{
Token: "not-a-token",
}
client, _ := tfe.NewClient(config)
_, emptyRunID := MockAllRunEvents(t, client)
ctx := context.TODO()
err := b.renderRunWarnings(ctx, client, emptyRunID)
if err != nil {
t.Fatalf("Expected to not error but received %s", err)
}
output := b.CLI.(*cli.MockUi).ErrorWriter.String()
if output != "" {
t.Fatalf("Expected %q to be empty but it was not", output)
}
}
func TestRunEventWarningsWithError(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
config := &tfe.Config{
Token: "not-a-token",
}
client, _ := tfe.NewClient(config)
MockAllRunEvents(t, client)
ctx := context.TODO()
err := b.renderRunWarnings(ctx, client, "bad run id")
if err == nil {
t.Error("Expected to error but did not")
}
}

View File

@ -241,6 +241,7 @@ func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.Resp
b.client.PolicySetOutcomes = mc.PolicySetOutcomes
b.client.PolicyChecks = mc.PolicyChecks
b.client.Runs = mc.Runs
b.client.RunEvents = mc.RunEvents
b.client.StateVersions = mc.StateVersions
b.client.StateVersionOutputs = mc.StateVersionOutputs
b.client.Variables = mc.Variables
@ -312,6 +313,7 @@ func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) {
b.client.PolicySetOutcomes = mc.PolicySetOutcomes
b.client.PolicyChecks = mc.PolicyChecks
b.client.Runs = mc.Runs
b.client.RunEvents = mc.RunEvents
b.client.StateVersions = mc.StateVersions
b.client.Variables = mc.Variables
b.client.Workspaces = mc.Workspaces

View File

@ -34,6 +34,7 @@ type MockClient struct {
RedactedPlans *MockRedactedPlans
PolicyChecks *MockPolicyChecks
Runs *MockRuns
RunEvents *MockRunEvents
StateVersions *MockStateVersions
StateVersionOutputs *MockStateVersionOutputs
Variables *MockVariables
@ -51,6 +52,7 @@ func NewMockClient() *MockClient {
c.PolicySetOutcomes = newMockPolicySetOutcomes(c)
c.PolicyChecks = newMockPolicyChecks(c)
c.Runs = newMockRuns(c)
c.RunEvents = newMockRunEvents(c)
c.StateVersions = newMockStateVersions(c)
c.StateVersionOutputs = newMockStateVersionOutputs(c)
c.Variables = newMockVariables(c)
@ -1168,6 +1170,31 @@ func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDis
return nil
}
type MockRunEvents struct{}
func newMockRunEvents(_ *MockClient) *MockRunEvents {
return &MockRunEvents{}
}
// List all the runs events of the given run.
func (m *MockRunEvents) List(ctx context.Context, runID string, options *tfe.RunEventListOptions) (*tfe.RunEventList, error) {
return &tfe.RunEventList{
Items: []*tfe.RunEvent{},
}, nil
}
func (m *MockRunEvents) Read(ctx context.Context, runEventID string) (*tfe.RunEvent, error) {
return m.ReadWithOptions(ctx, runEventID, nil)
}
func (m *MockRunEvents) ReadWithOptions(ctx context.Context, runEventID string, options *tfe.RunEventReadOptions) (*tfe.RunEvent, error) {
return &tfe.RunEvent{
ID: GenerateID("re-"),
Action: "created",
CreatedAt: time.Now(),
}, nil
}
type MockStateVersions struct {
client *MockClient
states map[string][]byte