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

schema/apply: handle error for multiple targets #92

Merged
merged 3 commits into from
Sep 12, 2024
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
65 changes: 35 additions & 30 deletions atlasexec/atlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,38 +248,21 @@ func (c *Client) runCommand(ctx context.Context, args []string) (io.Reader, erro
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
cerr := &Error{
return nil, &Error{
err: err,
Stderr: strings.TrimSpace(stderr.String()),
Stdout: strings.TrimSpace(stdout.String()),
}
if exitErr := (&exec.ExitError{}); errors.As(err, &exitErr) {
cerr.err = exitErr
}
return nil, cerr
}
return &stdout, nil
}

// TempFile creates a temporary file with the given content and extension.
func TempFile(content, ext string) (string, func() error, error) {
f, err := os.CreateTemp("", "atlasexec-*."+ext)
if err != nil {
return "", nil, err
}
defer f.Close()
_, err = f.WriteString(content)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("file://%s", f.Name()), func() error {
return os.Remove(f.Name())
}, nil
}

// Error is an error returned by the atlasexec package,
// when it executes the atlas-cli command.
type Error struct {
err *exec.ExitError
Stdout string
Stderr string
err error // The underlying error.
Stdout string // Stdout of the command.
Stderr string // Stderr of the command.
}

// Error implements the error interface.
Expand All @@ -291,17 +274,38 @@ func (e *Error) Error() string {
}

// ExitCode returns the exit code of the command.
// If the error is not an exec.ExitError, it returns 1.
func (e *Error) ExitCode() int {
if e.err == nil {
return new(exec.ExitError).ExitCode()
var exitErr *exec.ExitError
if errors.As(e.err, &exitErr) {
return exitErr.ExitCode()
}
return e.err.ExitCode()
// Not an exec.ExitError or nil.
// Return the system default exit code.
return new(exec.ExitError).ExitCode()
}

// Unwrap returns the underlying error.
func (e *Error) Unwrap() error {
return e.err
}

// TempFile creates a temporary file with the given content and extension.
func TempFile(content, ext string) (string, func() error, error) {
f, err := os.CreateTemp("", "atlasexec-*."+ext)
if err != nil {
return "", nil, err
}
defer f.Close()
_, err = f.WriteString(content)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("file://%s", f.Name()), func() error {
return os.Remove(f.Name())
}, nil
}

// AsArgs returns the variables as arguments.
func (v Vars2) AsArgs() []string {
keys := make([]string, 0, len(v))
Expand Down Expand Up @@ -363,19 +367,20 @@ func jsonDecode[T any](r io.Reader, err error) ([]*T, error) {
dst = append(dst, &m)
default:
return nil, &Error{
err: fmt.Errorf("decoding JSON from stdout: %w", err),
Stdout: string(buf),
}
}
}
}

func jsonDecodeErr[T any](fn func([]*T) error) func(io.Reader, error) ([]*T, error) {
func jsonDecodeErr[T any](fn func([]*T, string) error) func(io.Reader, error) ([]*T, error) {
return func(r io.Reader, err error) ([]*T, error) {
if err != nil {
if cliErr := (&Error{}); errors.As(err, &cliErr) && cliErr.Stderr == "" {
if cliErr := (&Error{}); errors.As(err, &cliErr) && cliErr.Stdout != "" {
d, err := jsonDecode[T](strings.NewReader(cliErr.Stdout), nil)
if err == nil {
return nil, fn(d)
return nil, fn(d, cliErr.Stderr)
}
// If the error is not a JSON, return the original error.
}
Expand Down
12 changes: 9 additions & 3 deletions atlasexec/atlas_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type (
// during a migration applying attempt.
MigrateApplyError struct {
Result []*MigrateApply
Stderr string
}
// MigrateExecOrder define how Atlas computes and executes pending migration files to the database.
// See: https://atlasgo.io/versioned/apply#execution-order
Expand Down Expand Up @@ -569,12 +570,17 @@ func (r MigrateStatus) Amount(version string) (amount uint64, ok bool) {
return amount, false
}

func newMigrateApplyError(r []*MigrateApply) error {
return &MigrateApplyError{Result: r}
func newMigrateApplyError(r []*MigrateApply, stderr string) error {
return &MigrateApplyError{Result: r, Stderr: stderr}
}

// Error implements the error interface.
func (e *MigrateApplyError) Error() string { return last(e.Result).Error }
func (e *MigrateApplyError) Error() string {
if e.Stderr != "" {
return e.Stderr
}
return last(e.Result).Error
}

func plural(n int) (s string) {
if n > 1 {
Expand Down
12 changes: 9 additions & 3 deletions atlasexec/atlas_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type (
// during a schema applying attempt.
SchemaApplyError struct {
Result []*SchemaApply
Stderr string
}
// SchemaInspectParams are the parameters for the `schema inspect` command.
SchemaInspectParams struct {
Expand Down Expand Up @@ -629,9 +630,14 @@ type InvalidParamsError struct {
func (e *InvalidParamsError) Error() string {
return fmt.Sprintf("atlasexec: command %q has invalid parameters: %v", e.cmd, e.msg)
}
func newSchemaApplyError(r []*SchemaApply) error {
return &SchemaApplyError{Result: r}
func newSchemaApplyError(r []*SchemaApply, stderr string) error {
return &SchemaApplyError{Result: r, Stderr: stderr}
}

// Error implements the error interface.
func (e *SchemaApplyError) Error() string { return last(e.Result).Error }
func (e *SchemaApplyError) Error() string {
if e.Stderr != "" {
return e.Stderr
}
return last(e.Result).Error
}
32 changes: 32 additions & 0 deletions atlasexec/atlas_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,35 @@ func TestSchema_Apply(t *testing.T) {
})
}
}

func TestSchema_ApplyEnvs(t *testing.T) {
wd, err := os.Getwd()
require.NoError(t, err)
c, err := atlasexec.NewClient(t.TempDir(), filepath.Join(wd, "./mock-atlas.sh"))
require.NoError(t, err)
require.NoError(t, c.SetEnv(map[string]string{
"TEST_ARGS": "schema apply --format {{ json . }} --env test",
"TEST_STDOUT": `{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Host":"local-su.db"}}
{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Host":"local-pi.db"}}
{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Host":"local-bu.db"}}`,
"TEST_STDERR": `Abort: The plan "From" hash does not match the current state hash (passed with --from):

- iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE= (plan value)
+ Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4= (current hash)

`,
}))
result, err := c.SchemaApply(context.Background(), &atlasexec.SchemaApplyParams{
Env: "test",
})
require.ErrorContains(t, err, `The plan "From" hash does not match the current state hash`)
require.Nil(t, result)

err2, ok := err.(*atlasexec.SchemaApplyError)
require.True(t, ok, "should be a SchemaApplyError, got %T", err)
require.Contains(t, err2.Stderr, `Abort: The plan "From" hash does not match the current state hash (passed with --from)`)
require.Len(t, err2.Result, 3, "should returns succeed results")
require.Equal(t, "sqlite://local-su.db", err2.Result[0].URL.String())
require.Equal(t, "sqlite://local-pi.db", err2.Result[1].URL.String())
require.Equal(t, "sqlite://local-bu.db", err2.Result[2].URL.String())
}
9 changes: 8 additions & 1 deletion atlasexec/mock-atlas.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ fi

if [[ "$TEST_STDOUT" != "" ]]; then
echo -n $TEST_STDOUT
exit 0
if [[ "$TEST_STDERR" == "" ]]; then
exit 0 # No stderr
fi
# In some cases, Atlas will write the error in stderr
# when if the command is partially successful.
# eg. Run the apply commands with multiple environments.
>&2 echo -n $TEST_STDERR
exit 1
fi

TEST_STDERR="${TEST_STDERR:-Missing stderr either stdout input for the test}"
Expand Down
Loading