From d960a2515f022ed50b3e428b26a96460253e12a7 Mon Sep 17 00:00:00 2001 From: "Giau. Tran Minh" Date: Thu, 12 Sep 2024 02:02:38 +0700 Subject: [PATCH 1/3] atlasexec: update Atlas mock --- atlasexec/mock-atlas.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/atlasexec/mock-atlas.sh b/atlasexec/mock-atlas.sh index be05c3e..ce13182 100755 --- a/atlasexec/mock-atlas.sh +++ b/atlasexec/mock-atlas.sh @@ -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}" From 85555ac6ba6bce35dfc4945e842fc74d73df738f Mon Sep 17 00:00:00 2001 From: "Giau. Tran Minh" Date: Thu, 12 Sep 2024 02:56:59 +0700 Subject: [PATCH 2/3] atlasexec: decode the stdout when got error --- atlasexec/atlas.go | 65 ++++++++++++++++++++------------------ atlasexec/atlas_migrate.go | 12 +++++-- atlasexec/atlas_schema.go | 12 +++++-- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/atlasexec/atlas.go b/atlasexec/atlas.go index 3854b30..f63aadf 100644 --- a/atlasexec/atlas.go +++ b/atlasexec/atlas.go @@ -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. @@ -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)) @@ -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. } diff --git a/atlasexec/atlas_migrate.go b/atlasexec/atlas_migrate.go index 362cdda..749a2cd 100644 --- a/atlasexec/atlas_migrate.go +++ b/atlasexec/atlas_migrate.go @@ -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 @@ -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 { diff --git a/atlasexec/atlas_schema.go b/atlasexec/atlas_schema.go index 58f08eb..dfc9313 100644 --- a/atlasexec/atlas_schema.go +++ b/atlasexec/atlas_schema.go @@ -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 { @@ -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 +} From 57bc32dbc7958c3273006a7a0984a5569c8a1d61 Mon Sep 17 00:00:00 2001 From: "Giau. Tran Minh" Date: Thu, 12 Sep 2024 13:17:03 +0700 Subject: [PATCH 3/3] atlasexec: add tests for multiple targets --- atlasexec/atlas_schema_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/atlasexec/atlas_schema_test.go b/atlasexec/atlas_schema_test.go index 42da8e8..329085c 100644 --- a/atlasexec/atlas_schema_test.go +++ b/atlasexec/atlas_schema_test.go @@ -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()) +}