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

Added ability for deferred mutator execution #380

Merged
merged 6 commits into from
May 16, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions bundle/deferred.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package bundle

import (
"context"

"github.com/databricks/bricks/libs/errs"
)

type DeferredMutator struct {
mutators []Mutator
finally []Mutator
}

func (d *DeferredMutator) Name() string {
return "deferred"
}

func Defer(mutators []Mutator, finally []Mutator) []Mutator {
return []Mutator{
&DeferredMutator{
mutators: mutators,
finally: finally,
},
}
}

func (d *DeferredMutator) Apply(ctx context.Context, b *Bundle) ([]Mutator, error) {
mainErr := Apply(ctx, b, d.mutators)
errOnFinish := Apply(ctx, b, d.finally)
if mainErr != nil || errOnFinish != nil {
return nil, errs.FromMany(mainErr, errOnFinish)
}

return nil, nil
}
108 changes: 108 additions & 0 deletions bundle/deferred_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package bundle

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

type mutatorWithError struct {
applyCalled int
errorMsg string
}

func (t *mutatorWithError) Name() string {
return "mutatorWithError"
}

func (t *mutatorWithError) Apply(_ context.Context, b *Bundle) ([]Mutator, error) {
t.applyCalled++
return nil, fmt.Errorf(t.errorMsg)
}

func TestDeferredMutatorWhenAllMutatorsSucceed(t *testing.T) {
m1 := &testMutator{}
m2 := &testMutator{}
m3 := &testMutator{}
cleanup := &testMutator{}
deferredMutator := Defer([]Mutator{m1, m2, m3}, []Mutator{cleanup})

bundle := &Bundle{}
err := Apply(context.Background(), bundle, deferredMutator)
assert.NoError(t, err)

assert.Equal(t, 1, m1.applyCalled)
assert.Equal(t, 1, m2.applyCalled)
assert.Equal(t, 1, m3.applyCalled)
assert.Equal(t, 1, cleanup.applyCalled)
}

func TestDeferredMutatorWhenFirstFails(t *testing.T) {
m1 := &testMutator{}
m2 := &testMutator{}
mErr := &mutatorWithError{errorMsg: "mutator error occurred"}
cleanup := &testMutator{}
deferredMutator := Defer([]Mutator{mErr, m1, m2}, []Mutator{cleanup})

bundle := &Bundle{}
err := Apply(context.Background(), bundle, deferredMutator)
assert.ErrorContains(t, err, "mutator error occurred")

assert.Equal(t, 1, mErr.applyCalled)
assert.Equal(t, 0, m1.applyCalled)
assert.Equal(t, 0, m2.applyCalled)
assert.Equal(t, 1, cleanup.applyCalled)
}

func TestDeferredMutatorWhenMiddleOneFails(t *testing.T) {
m1 := &testMutator{}
m2 := &testMutator{}
mErr := &mutatorWithError{errorMsg: "mutator error occurred"}
cleanup := &testMutator{}
deferredMutator := Defer([]Mutator{m1, mErr, m2}, []Mutator{cleanup})

bundle := &Bundle{}
err := Apply(context.Background(), bundle, deferredMutator)
assert.ErrorContains(t, err, "mutator error occurred")

assert.Equal(t, 1, m1.applyCalled)
assert.Equal(t, 1, mErr.applyCalled)
assert.Equal(t, 0, m2.applyCalled)
assert.Equal(t, 1, cleanup.applyCalled)
}

func TestDeferredMutatorWhenLastOneFails(t *testing.T) {
m1 := &testMutator{}
m2 := &testMutator{}
mErr := &mutatorWithError{errorMsg: "mutator error occurred"}
cleanup := &testMutator{}
deferredMutator := Defer([]Mutator{m1, m2, mErr}, []Mutator{cleanup})

bundle := &Bundle{}
err := Apply(context.Background(), bundle, deferredMutator)
assert.ErrorContains(t, err, "mutator error occurred")

assert.Equal(t, 1, m1.applyCalled)
assert.Equal(t, 1, m2.applyCalled)
assert.Equal(t, 1, mErr.applyCalled)
assert.Equal(t, 1, cleanup.applyCalled)
}

func TestDeferredMutatorCombinesErrorMessages(t *testing.T) {
m1 := &testMutator{}
m2 := &testMutator{}
mErr := &mutatorWithError{errorMsg: "mutator error occurred"}
cleanupErr := &mutatorWithError{errorMsg: "cleanup error occurred"}
deferredMutator := Defer([]Mutator{m1, m2, mErr}, []Mutator{cleanupErr})

bundle := &Bundle{}
err := Apply(context.Background(), bundle, deferredMutator)
assert.ErrorContains(t, err, "mutator error occurred\ncleanup error occurred")

assert.Equal(t, 1, m1.applyCalled)
assert.Equal(t, 1, m2.applyCalled)
assert.Equal(t, 1, mErr.applyCalled)
assert.Equal(t, 1, cleanupErr.applyCalled)
}
25 changes: 14 additions & 11 deletions bundle/phases/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ import (

// The deploy phase deploys artifacts and resources.
func Deploy() bundle.Mutator {
deployPhase := bundle.Defer([]bundle.Mutator{
lock.Acquire(),
files.Upload(),
artifacts.UploadAll(),
terraform.Interpolate(),
terraform.Write(),
terraform.StatePull(),
terraform.Apply(),
terraform.StatePush(),
}, []bundle.Mutator{
lock.Release(),
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This makes me think we also need (separate PR of course) a bundle.Seq to execute multiple mutators in sequence such that we can compose lock.Acquire with a bundle.Defer where the unlock happens. As it is here, lock.Release would always be called even if lock.Acquire fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pietern just to clarify to make sure I understand correctly. You propose to have a new method bundle.Seq with API like this

func Seq(mutators []Mutator) Mutator { ... }

seq := bundle.Seq(
		bundle.Defer([]bundle.Mutator{
			files.Upload(),
			artifacts.UploadAll(),
			terraform.Interpolate(),
			terraform.Write(),
			terraform.StatePull(),
			terraform.Apply(),
			terraform.StatePush(),
		}, []bundle.Mutator{
			lock.Release(),
		}))

deployPhase := []bundle.Mutator{
    lock.Acquire(),
    seq,
}

bundle.Seq will essentially transform list of mutators into Mutator object with Apply method so it can be added to the list of mutators

In such case seq will only be executed once lock.Acquire succeeds

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes to the proposed prototype, but the call site would be slightly different and call the Seq function to compose call lock.Acquire and the defer block in sequence.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So something like this then?

deferred := bundle.Defer([]bundle.Mutator{
			files.Upload(),
			artifacts.UploadAll(),
			terraform.Interpolate(),
			terraform.Write(),
			terraform.StatePull(),
			terraform.Apply(),
			terraform.StatePush(),
		}, []bundle.Mutator{
			lock.Release(),
		})

seq := bundle.Seq([lock.Acquire(), deferred])

deployPhase := []bundle.Mutator{
   seq
}

Do we then really need bundle.Seq? What if bundle.Defer just returns Mutator instead of []Mutator?
In that case we would just have it used like

deployPhase := []bundle.Mutator{
   lock.Acquire(),
   deferred
}

@pietern any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't if we leave everything else the same, so yeah you could leave as is for this PR.

The comment on bundle.Seq is because we have mixed usage of singular Mutator and []Mutator in a couple places and formalizing []Mutator into a bundle.Seq would get rid of that.


return newPhase(
"deploy",
[]bundle.Mutator{
lock.Acquire(),
files.Upload(),
artifacts.UploadAll(),
terraform.Interpolate(),
terraform.Write(),
terraform.StatePull(),
terraform.Apply(),
terraform.StatePush(),
lock.Release(),
},
deployPhase,
)
}
21 changes: 12 additions & 9 deletions bundle/phases/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import (

// The destroy phase deletes artifacts and resources.
func Destroy() bundle.Mutator {
destroyPhase := bundle.Defer([]bundle.Mutator{
lock.Acquire(),
terraform.StatePull(),
terraform.Plan(terraform.PlanGoal("destroy")),
terraform.Destroy(),
terraform.StatePush(),
files.Delete(),
}, []bundle.Mutator{
lock.Release(),
})

return newPhase(
"destroy",
[]bundle.Mutator{
lock.Acquire(),
terraform.StatePull(),
terraform.Plan(terraform.PlanGoal("destroy")),
terraform.Destroy(),
terraform.StatePush(),
lock.Release(),
files.Delete(),
},
destroyPhase,
)
}
68 changes: 68 additions & 0 deletions libs/errs/aggregate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package errs

import "errors"

type aggregateError struct {
errors []error
}

func FromMany(errors ...error) error {
n := 0
for _, err := range errors {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
aggregateErr := &aggregateError{
errors: make([]error, 0, n),
}
for _, err := range errors {
if err != nil {
aggregateErr.errors = append(aggregateErr.errors, err)
}
}
return aggregateErr
}

func (ce *aggregateError) Error() string {
var b []byte
for i, err := range ce.errors {
if i > 0 {
b = append(b, '\n')
}
b = append(b, err.Error()...)
}
return string(b)
}

func (ce *aggregateError) Unwrap() error {
return errorChain(ce.errors)
}

// Represents chained list of errors.
// Implements Error interface so that chain of errors
// can correctly work with errors.Is/As method
type errorChain []error

func (ec errorChain) Error() string {
return ec[0].Error()
}

func (ec errorChain) Unwrap() error {
if len(ec) == 1 {
return nil
}

return ec[1:]
}

func (ec errorChain) As(target interface{}) bool {
return errors.As(ec[0], target)
}

func (ec errorChain) Is(target error) bool {
return errors.Is(ec[0], target)
}
37 changes: 37 additions & 0 deletions libs/errs/aggregate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package errs

import (
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestFromManyErrors(t *testing.T) {
e1 := fmt.Errorf("Error 1")
e2 := fmt.Errorf("Error 2")
e3 := fmt.Errorf("Error 3")
err := FromMany(e1, e2, e3)

assert.True(t, errors.Is(err, e1))
assert.True(t, errors.Is(err, e2))
assert.True(t, errors.Is(err, e3))

assert.Equal(t, err.Error(), `Error 1
Error 2
Error 3`)
}

func TestFromManyErrorsWihtNil(t *testing.T) {
e1 := fmt.Errorf("Error 1")
var e2 error = nil
e3 := fmt.Errorf("Error 3")
err := FromMany(e1, e2, e3)

assert.True(t, errors.Is(err, e1))
assert.True(t, errors.Is(err, e3))

assert.Equal(t, err.Error(), `Error 1
Error 3`)
}