diff --git a/cmd/atlas/internal/cloudapi/client.go b/cmd/atlas/internal/cloudapi/client.go index 83364cbc58c..3accdd436a8 100644 --- a/cmd/atlas/internal/cloudapi/client.go +++ b/cmd/atlas/internal/cloudapi/client.go @@ -165,17 +165,17 @@ type ( ) // ReportMigrationSet reports a set of migration deployments to the Atlas Cloud API. -func (c *Client) ReportMigrationSet(ctx context.Context, input ReportMigrationSetInput) error { +func (c *Client) ReportMigrationSet(ctx context.Context, input ReportMigrationSetInput) (string, error) { var ( payload struct { ReportMigrationSet struct { - Success bool `json:"success"` + URL string `json:"url"` } `json:"reportMigrationSet"` } query = ` mutation ReportMigrationSet($input: ReportMigrationSetInput!) { reportMigrationSet(input: $input) { - success + url } }` vars = struct { @@ -184,21 +184,24 @@ func (c *Client) ReportMigrationSet(ctx context.Context, input ReportMigrationSe Input: input, } ) - return c.post(ctx, query, vars, &payload) + if err := c.post(ctx, query, vars, &payload); err != nil { + return "", err + } + return payload.ReportMigrationSet.URL, nil } // ReportMigration reports a migration deployment to the Atlas Cloud API. -func (c *Client) ReportMigration(ctx context.Context, input ReportMigrationInput) error { +func (c *Client) ReportMigration(ctx context.Context, input ReportMigrationInput) (string, error) { var ( payload struct { ReportMigration struct { - Success bool `json:"success"` + URL string `json:"url"` } `json:"reportMigration"` } query = ` mutation ReportMigration($input: ReportMigrationInput!) { reportMigration(input: $input) { - success + url } }` vars = struct { @@ -207,7 +210,10 @@ func (c *Client) ReportMigration(ctx context.Context, input ReportMigrationInput Input: input, } ) - return c.post(ctx, query, vars, &payload) + if err := c.post(ctx, query, vars, &payload); err != nil { + return "", err + } + return payload.ReportMigration.URL, nil } func (c *Client) post(ctx context.Context, query string, vars, data any) error { diff --git a/cmd/atlas/internal/cloudapi/client_test.go b/cmd/atlas/internal/cloudapi/client_test.go index d234a05e4a4..9e048c38cf8 100644 --- a/cmd/atlas/internal/cloudapi/client_test.go +++ b/cmd/atlas/internal/cloudapi/client_test.go @@ -59,11 +59,12 @@ func TestClient_Error(t *testing.T) { })) client := New(srv.URL, "atlas") defer srv.Close() - err := client.ReportMigration(context.Background(), ReportMigrationInput{ + link, err := client.ReportMigration(context.Background(), ReportMigrationInput{ EnvName: "foo", ProjectName: "bar", }) require.EqualError(t, err, "variable.input.driver error", "error is trimmed") + require.Empty(t, link) } func TestClient_ReportMigration(t *testing.T) { @@ -78,14 +79,16 @@ func TestClient_ReportMigration(t *testing.T) { require.NoError(t, err) require.Equal(t, env, input.Variables.Input.EnvName) require.Equal(t, project, input.Variables.Input.ProjectName) + fmt.Fprintf(w, `{"data":{"reportMigration":{"url":"https://atlas.com"}}}`) })) client := New(srv.URL, "atlas") defer srv.Close() - err := client.ReportMigration(context.Background(), ReportMigrationInput{ + link, err := client.ReportMigration(context.Background(), ReportMigrationInput{ EnvName: env, ProjectName: project, }) require.NoError(t, err) + require.NotEmpty(t, link) } func TestClient_ReportMigrationSet(t *testing.T) { @@ -110,10 +113,11 @@ func TestClient_ReportMigrationSet(t *testing.T) { require.Equal(t, env, input.Variables.Input.Completed[1].EnvName) require.Equal(t, project, input.Variables.Input.Completed[1].ProjectName) require.Equal(t, "dir-2", input.Variables.Input.Completed[1].DirName) + fmt.Fprintf(w, `{"data":{"reportMigrationSet":{"url":"https://atlas.com"}}}`) })) client := New(srv.URL, "atlas") defer srv.Close() - err := client.ReportMigrationSet(context.Background(), ReportMigrationSetInput{ + link, err := client.ReportMigrationSet(context.Background(), ReportMigrationSetInput{ ID: id, Planned: planned, Log: []ReportStep{{Text: log}}, @@ -131,4 +135,5 @@ func TestClient_ReportMigrationSet(t *testing.T) { }, }) require.NoError(t, err) + require.NotEmpty(t, link) } diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go index 1095b4d5749..00df632d441 100644 --- a/cmd/atlas/internal/cmdapi/migrate.go +++ b/cmd/atlas/internal/cmdapi/migrate.go @@ -361,7 +361,10 @@ func (s *MigrateReportSet) Flush(cmd *cobra.Command, cmdErr error) { if cmdErr != nil && s.Error == nil { s.StepLogError(cmdErr.Error()) } - var err error + var ( + err error + link string + ) switch { // Skip reporting if set is empty, // or there is no cloud connectivity. @@ -369,23 +372,28 @@ func (s *MigrateReportSet) Flush(cmd *cobra.Command, cmdErr error) { return // Single migration that was completed. case s.Planned == 1 && len(s.Completed) == 1: - err = s.client.ReportMigration(cmd.Context(), s.Completed[0]) + link, err = s.client.ReportMigration(cmd.Context(), s.Completed[0]) // Single migration that failed to start. case s.Planned == 1 && len(s.Completed) == 0: s.EndTime = time.Now() - err = s.client.ReportMigrationSet(cmd.Context(), s.ReportMigrationSetInput) + link, err = s.client.ReportMigrationSet(cmd.Context(), s.ReportMigrationSetInput) // Multi environment migration (e.g., multi-tenancy). case s.Planned > 1: s.EndTime = time.Now() - err = s.client.ReportMigrationSet(cmd.Context(), s.ReportMigrationSetInput) + link, err = s.client.ReportMigrationSet(cmd.Context(), s.ReportMigrationSetInput) } - if err != nil { + switch { + case err != nil: txt := fmt.Sprintf("Error: %s", strings.TrimRight(err.Error(), "\n")) // Ensure errors are printed in new lines. if cmd.Flags().Changed(flagFormat) { txt = "\n" + txt } cmd.PrintErrln(txt) + // Unlike errors that are printed to stderr, links are printed to stdout. + // We do it only if the format was not customized by the user (e.g., JSON). + case link != "" && !cmd.Flags().Changed(flagFormat): + cmd.Println(link) } } @@ -397,7 +405,7 @@ func (r *MigrateReport) Init(c *sqlclient.Client, l *cmdlog.MigrateApply, rrw cm // RecordTargetID asks the revisions-table to allow or provide // the target identifier if cloud reporting is enabled. func (r *MigrateReport) RecordTargetID(ctx context.Context) error { - if r.CloudEnabled() { + if r.CloudEnabled(ctx) { id, err := r.rrw.ID(ctx, operatorVersion()) if err != nil { return err @@ -409,7 +417,7 @@ func (r *MigrateReport) RecordTargetID(ctx context.Context) error { // Done closes and flushes this report. func (r *MigrateReport) Done(cmd *cobra.Command, flags migrateApplyFlags) error { - if !r.CloudEnabled() { + if !r.CloudEnabled(cmd.Context()) { return logApply(cmd, cmd.OutOrStdout(), flags, r.log) } var ( @@ -470,8 +478,8 @@ func (r *MigrateReport) Done(cmd *cobra.Command, flags migrateApplyFlags) error } // CloudEnabled reports if cloud reporting is enabled. -func (r *MigrateReport) CloudEnabled() bool { - return r.env != nil && r.env.cfg != nil && r.env.cfg.Client != nil && r.env.cfg.Project != "" +func (r *MigrateReport) CloudEnabled(ctx context.Context) bool { + return r.env != nil && r.env.cfg != nil && r.env.cfg.Project != "" && (r.env.cfg.Client != nil || cloudapi.FromContext(ctx) != nil) } func logApply(cmd *cobra.Command, w io.Writer, flags migrateApplyFlags, r *cmdlog.MigrateApply) error { diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go index c70b1e81c9e..0ea805cc38f 100644 --- a/cmd/atlas/internal/cmdapi/migrate_test.go +++ b/cmd/atlas/internal/cmdapi/migrate_test.go @@ -818,11 +818,13 @@ func TestMigrate_ApplyCloudReport(t *testing.T) { w.WriteHeader(status) } require.NoError(t, json.Unmarshal(m.Variables.Input, &reports)) + fmt.Fprint(w, `{"data":{"reportMigrationSet":{"url": "https://gh.atlasgo.cloud/deployments/sets/94489280524"}}}`) case strings.Contains(m.Query, "mutation") && strings.Contains(m.Query, "ReportMigration"): if status != 0 { w.WriteHeader(status) } require.NoError(t, json.Unmarshal(m.Variables.Input, &report)) + fmt.Fprint(w, `{"data":{"reportMigration":{"url": "https://gh.atlasgo.cloud/deployments/51539607559"}}}`) default: t.Fatalf("unexpected query: %s", m.Query) } @@ -869,7 +871,7 @@ env { "--var", "cloud_url="+srv.URL, ) require.NoError(t, err) - require.Equal(t, "No migration files to execute\n", s) + require.Equal(t, "No migration files to execute\nhttps://gh.atlasgo.cloud/deployments/51539607559\n", s) require.NotEmpty(t, report.Target.ID) _, err = uuid.Parse(report.Target.ID) require.NoError(t, err, "target id is not a valid uuid") @@ -908,7 +910,7 @@ env { require.NoError(t, err) // Reporting does not affect the output. require.True(t, strings.HasPrefix(s, "Migrating to version 2 (2 migrations in total):")) - require.True(t, strings.HasSuffix(s, " -- 2 migrations \n -- 2 sql statements\n")) + require.True(t, strings.HasSuffix(s, " -- 2 migrations \n -- 2 sql statements\nhttps://gh.atlasgo.cloud/deployments/51539607559\n")) require.Equal(t, "", report.FromVersion, "from empty database") require.Equal(t, "2", report.ToVersion) require.Equal(t, "2", report.CurrentVersion) @@ -998,6 +1000,8 @@ func TestMigrate_ApplyCloudReportSet(t *testing.T) { case strings.Contains(m.Query, "mutation"): if status != 0 { w.WriteHeader(status) + } else { + fmt.Fprint(w, `{"data":{"reportMigrationSet":{"url":"https://gh.atlasgo.cloud/deployments/sets/94489280524"}}}`) } require.NoError(t, json.Unmarshal(m.Variables.Input, &report)) default: @@ -1053,7 +1057,7 @@ env { "--var", "cloud_url="+srv.URL, ) require.NoError(t, err) - require.Equal(t, "No migration files to execute\nNo migration files to execute\n", s) + require.Equal(t, "No migration files to execute\nNo migration files to execute\nhttps://gh.atlasgo.cloud/deployments/sets/94489280524\n", s) require.NotEmpty(t, report.ID) _, err = uuid.Parse(report.ID) require.NoError(t, err, "set id is not a valid uuid")