Emit diagnostics for errors coming from step generation (#1766)

The plan executor assumed that the step generator was responsible for
logging its own diagnostics, which it sort-of is but also doesn't log a
majority of the diagnositcs that come out of it. This commit logs all
errors coming out of step generation so that we don't unintentionally
drop errors.
This commit is contained in:
Sean Gillespie 2018-08-13 13:41:01 -07:00 committed by GitHub
parent e51e159d18
commit d9c56eab23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 146 additions and 21 deletions

View file

@ -16,12 +16,15 @@ package engine
import (
"context"
"strings"
"testing"
"github.com/blang/semver"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/resource/deploy"
@ -210,7 +213,7 @@ func (u *updateInfo) GetTarget() *deploy.Target {
}
type TestOp func(UpdateInfo, *Context, UpdateOptions, bool) (ResourceChanges, error)
type ValidateFunc func(project workspace.Project, target deploy.Target, j *Journal, err error) error
type ValidateFunc func(project workspace.Project, target deploy.Target, j *Journal, events []Event, err error) error
func (op TestOp) Run(project workspace.Project, target deploy.Target, opts UpdateOptions,
dryRun bool, validate ValidateFunc) (*deploy.Snapshot, error) {
@ -229,8 +232,10 @@ func (op TestOp) Run(project workspace.Project, target deploy.Target, opts Updat
}
// Begin draining events.
var firedEvents []Event
go func() {
for range events {
for e := range events {
firedEvents = append(firedEvents, e)
}
}()
@ -242,19 +247,21 @@ func (op TestOp) Run(project workspace.Project, target deploy.Target, opts Updat
return nil, err
}
if validate != nil {
err = validate(project, target, journal, err)
err = validate(project, target, journal, firedEvents, err)
}
snap := journal.Snap(target.Snapshot)
if snap != nil {
if err == nil && snap != nil {
err = snap.VerifyIntegrity()
}
return snap, err
}
type TestStep struct {
Op TestOp
Validate ValidateFunc
Op TestOp
ExpectFailure bool
SkipPreview bool
Validate ValidateFunc
}
type TestPlan struct {
@ -316,9 +323,23 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
}
for _, step := range p.Steps {
_, err := step.Op.Run(*project, *target, p.Options, true, step.Validate)
assert.NoError(t, err)
if !step.SkipPreview {
_, err := step.Op.Run(*project, *target, p.Options, true, step.Validate)
if step.ExpectFailure {
assert.Error(t, err)
continue
}
assert.NoError(t, err)
}
var err error
target.Snapshot, err = step.Op.Run(*project, *target, p.Options, false, step.Validate)
if step.ExpectFailure {
assert.Error(t, err)
continue
}
assert.NoError(t, err)
}
@ -330,7 +351,7 @@ func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
// Initial update
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see only creates.
for _, entry := range j.Entries {
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
@ -342,7 +363,7 @@ func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
// No-op refresh
{
Op: Refresh,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see only sames.
for _, entry := range j.Entries {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
@ -354,7 +375,7 @@ func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
// No-op update
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see only sames.
for _, entry := range j.Entries {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
@ -366,7 +387,7 @@ func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
// No-op refresh
{
Op: Refresh,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see only sames.
for _, entry := range j.Entries {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
@ -378,7 +399,7 @@ func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
// Destroy
{
Op: Destroy,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see only deletes.
for _, entry := range j.Entries {
assert.Equal(t, deploy.OpDelete, entry.Step.Op())
@ -390,7 +411,7 @@ func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
// No-op refresh
{
Op: Refresh,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
assert.Len(t, j.Entries, 0)
assert.Len(t, j.Snap(target.Snapshot).Resources, 0)
return err
@ -502,7 +523,7 @@ func TestSingleResourceDefaultProviderUpgrade(t *testing.T) {
}},
}
validate := func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
validate := func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see only sames: the default provider should be injected into the old state before the update
// runs.
for _, entry := range j.Entries {
@ -528,7 +549,7 @@ func TestSingleResourceDefaultProviderUpgrade(t *testing.T) {
// Run a single destroy step using the base snapshot.
p.Steps = []TestStep{{
Op: Destroy,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
// Should see two deletes: the default provider should be injected into the old state before the update
// runs.
deleted := make(map[resource.URN]bool)
@ -595,7 +616,7 @@ func TestSingleResourceDefaultProviderReplace(t *testing.T) {
p.Config[config.MustMakeKey("pkgA", "foo")] = config.NewValue("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
@ -682,7 +703,7 @@ func TestSingleResourceExplicitProviderReplace(t *testing.T) {
providerInputs[resource.PropertyKey("foo")] = resource.NewStringProperty("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
provURN := p.NewProviderURN("pkgA", "provA", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
@ -769,7 +790,7 @@ func TestSingleResourceExplicitProviderDeleteBeforeReplace(t *testing.T) {
providerInputs[resource.PropertyKey("foo")] = resource.NewStringProperty("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, err error) error {
Validate: func(project workspace.Project, target deploy.Target, j *Journal, _ []Event, err error) error {
provURN := p.NewProviderURN("pkgA", "provA", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
@ -865,7 +886,7 @@ func TestDestroyWithPendingDelete(t *testing.T) {
p.Steps = []TestStep{{
Op: Update,
Validate: func(_ workspace.Project, _ deploy.Target, j *Journal, err error) error {
Validate: func(_ workspace.Project, _ deploy.Target, j *Journal, _ []Event, err error) error {
// Verify that we see a DeleteReplacement for the resource with ID 0 and a Delete for the resouce with
// ID 1.
deletedID0, deletedID1 := false, false
@ -937,7 +958,7 @@ func TestUpdateWithPendingDelete(t *testing.T) {
p.Steps = []TestStep{{
Op: Destroy,
Validate: func(_ workspace.Project, _ deploy.Target, j *Journal, err error) error {
Validate: func(_ workspace.Project, _ deploy.Target, j *Journal, _ []Event, err error) error {
// Verify that we see a DeleteReplacement for the resource with ID 0 and a Delete for the resouce with
// ID 1.
deletedID0, deletedID1 := false, false
@ -1157,3 +1178,101 @@ func TestRefreshInitFailure(t *testing.T) {
}
}
}
// Test that ensures that we log diagnostics for resources that receive an error from Check. (Note that this
// is distinct from receiving non-error failures from Check.)
func TestCheckFailureRecord(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CheckF: func(urn resource.URN,
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
return nil, nil, errors.New("oh no, check had an error")
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, "", false, nil, "", nil)
assert.Error(t, err)
return err
})
host := deploytest.NewPluginHost(nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{host: host},
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, evts []Event, err error) error {
sawFailure := false
for _, evt := range evts {
if evt.Type == DiagEvent {
e := evt.Payload.(DiagEventPayload)
msg := colors.Never.Colorize(e.Message)
sawFailure = msg == "oh no, check had an error\n" && e.Severity == diag.Error
}
}
assert.True(t, sawFailure)
return err
},
}},
}
p.Run(t, nil)
}
// Test that checks that we emit diagnostics for properties that check says are invalid.
func TestCheckFailureInvalidPropertyRecord(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CheckF: func(urn resource.URN,
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
return nil, []plugin.CheckFailure{{
Property: "someprop",
Reason: "field is not valid",
}}, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, "", false, nil, "", nil)
assert.Error(t, err)
return err
})
host := deploytest.NewPluginHost(nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{host: host},
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, j *Journal, evts []Event, err error) error {
sawFailure := false
for _, evt := range evts {
if evt.Type == DiagEvent {
e := evt.Payload.(DiagEventPayload)
msg := colors.Never.Colorize(e.Message)
sawFailure = strings.Contains(msg, "field is not valid") && e.Severity == diag.Error
if sawFailure {
break
}
}
}
assert.True(t, sawFailure)
return err
},
}},
}
p.Run(t, nil)
}

View file

@ -19,6 +19,7 @@ import (
"sync/atomic"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/util/logging"
)
@ -208,6 +209,9 @@ func (pe *PlanExecutor) handleSingleEvent(event SourceEvent) {
if steperr != nil {
logging.V(planExecutorLogLevel).Infof(
"PlanExecutor.handleSingleEvent(...): received step event error: %v", steperr.Error())
goal := e.Goal()
urn := pe.plan.generateURN(goal.Parent, goal.Type, goal.Name)
pe.plan.Diag().Errorf(diag.RawMessage(urn, steperr.Error()))
pe.cancel()
return
}
@ -219,6 +223,8 @@ func (pe *PlanExecutor) handleSingleEvent(event SourceEvent) {
if steperr != nil {
logging.V(planExecutorLogLevel).Infof(
"PlanExecutor.handleSingleEvent(...): received step event error: %v", steperr.Error())
urn := pe.plan.generateURN(e.Parent(), e.Type(), e.Name())
pe.plan.Diag().Errorf(diag.RawMessage(urn, steperr.Error()))
pe.cancel()
return
}