From a91e26b23a96b04e38ba618eefdd863448a0bb6e Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 13 Oct 2023 14:09:10 +0200 Subject: [PATCH 1/3] ovm-cli list-changes downloads metadata or everything for an account's changes --- .gitignore | 1 + cmd/listchanges.go | 298 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 cmd/listchanges.go diff --git a/.gitignore b/.gitignore index 8b12dafd..9508d870 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ go.work dist/ tracing/commit.txt cmd/commit.txt +output diff --git a/cmd/listchanges.go b/cmd/listchanges.go new file mode 100644 index 00000000..f3c008fe --- /dev/null +++ b/cmd/listchanges.go @@ -0,0 +1,298 @@ +package cmd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/bufbuild/connect-go" + "github.com/google/uuid" + "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/sdp-go" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// listChangesCmd represents the get-change command +var listChangesCmd = &cobra.Command{ + Use: "list-changes --dir ./output", + Short: "Displays the contents of a change.", + PreRun: func(cmd *cobra.Command, args []string) { + // Bind these to viper + err := viper.BindPFlags(cmd.Flags()) + if err != nil { + log.WithError(err).Fatal("could not bind `get-change` flags") + } + }, + Run: func(cmd *cobra.Command, args []string) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create a goroutine to watch for cancellation signals + go func() { + select { + case <-sigs: + cancel() + case <-ctx.Done(): + } + }() + + exitcode := ListChanges(ctx, nil) + tracing.ShutdownTracer() + os.Exit(exitcode) + }, +} + +func ListChanges(ctx context.Context, ready chan bool) int { + timeout, err := time.ParseDuration(viper.GetString("timeout")) + if err != nil { + log.Errorf("invalid --timeout value '%v', error: %v", viper.GetString("timeout"), err) + return 1 + } + + ctx, span := tracing.Tracer().Start(ctx, "CLI ListChanges", trace.WithAttributes( + attribute.String("om.config", fmt.Sprintf("%v", viper.AllSettings())), + )) + defer span.End() + + ctx, err = ensureToken(ctx, []string{"changes:read"}) + if err != nil { + log.WithContext(ctx).WithFields(log.Fields{ + "url": viper.GetString("url"), + }).WithError(err).Error("failed to authenticate") + return 1 + } + + // apply a timeout to the main body of processing + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + snapshots := AuthenticatedSnapshotsClient(ctx) + bookmarks := AuthenticatedBookmarkClient(ctx) + changes := AuthenticatedChangesClient(ctx) + + response, err := changes.ListChanges(ctx, &connect.Request[sdp.ListChangesRequest]{ + Msg: &sdp.ListChangesRequest{}, + }) + if err != nil { + log.WithContext(ctx).WithError(err).Error("failed to list changes") + return 1 + } + for _, change := range response.Msg.Changes { + changeUuid := uuid.UUID(change.Metadata.UUID) + log.WithContext(ctx).WithFields(log.Fields{ + "change-uuid": changeUuid, + "change-created": change.Metadata.CreatedAt.AsTime(), + "change-status": change.Metadata.Status.String(), + "change-name": change.Properties.Title, + "change-description": change.Properties.Description, + }).Info("found change") + + b, err := json.MarshalIndent(change.ToMap(), "", " ") + if err != nil { + log.WithContext(ctx).Errorf("Error rendering change: %v", err) + return 1 + } + + err = printJson(ctx, b, "change", changeUuid.String()) + if err != nil { + return 1 + } + + if viper.GetBool("fetch-data") { + ciUuid := uuid.UUID(change.Properties.ChangingItemsBookmarkUUID) + if ciUuid != uuid.Nil { + changingItems, err := bookmarks.GetBookmark(ctx, &connect.Request[sdp.GetBookmarkRequest]{ + Msg: &sdp.GetBookmarkRequest{ + UUID: ciUuid[:], + }, + }) + // continue processing if item not found + if connect.CodeOf(err) != connect.CodeNotFound { + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "change-uuid": changeUuid, + "changing-items-uuid": ciUuid.String(), + }).Error("failed to get ChangingItemsBookmark") + return 1 + } + + b, err := json.MarshalIndent(changingItems.Msg.Bookmark.ToMap(), "", " ") + if err != nil { + log.WithContext(ctx).WithFields(log.Fields{ + "change-uuid": changeUuid, + "changing-items-uuid": ciUuid.String(), + }).Errorf("Error rendering changing items bookmark: %v", err) + return 1 + } + + err = printJson(ctx, b, "changing-items", ciUuid.String()) + if err != nil { + return 1 + } + } + } + + brUuid := uuid.UUID(change.Properties.BlastRadiusSnapshotUUID) + if brUuid != uuid.Nil { + brSnap, err := snapshots.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{ + Msg: &sdp.GetSnapshotRequest{ + UUID: brUuid[:], + }, + }) + // continue processing if item not found + if connect.CodeOf(err) != connect.CodeNotFound { + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "change-uuid": changeUuid, + "blast-radius-uuid": brUuid.String(), + }).Error("failed to get BlastRadiusSnapshot") + return 1 + } + + b, err := json.MarshalIndent(brSnap.Msg.Snapshot.ToMap(), "", " ") + if err != nil { + log.WithContext(ctx).WithFields(log.Fields{ + "change-uuid": changeUuid, + "blast-radius-uuid": brUuid.String(), + }).Errorf("Error rendering blast radius snapshot: %v", err) + return 1 + } + + err = printJson(ctx, b, "blast-radius", brUuid.String()) + if err != nil { + return 1 + } + } + } + + sbsUuid := uuid.UUID(change.Properties.SystemBeforeSnapshotUUID) + if sbsUuid != uuid.Nil { + brSnap, err := snapshots.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{ + Msg: &sdp.GetSnapshotRequest{ + UUID: sbsUuid[:], + }, + }) + // continue processing if item not found + if connect.CodeOf(err) != connect.CodeNotFound { + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "change-uuid": changeUuid, + "system-before-uuid": sbsUuid.String(), + }).Error("failed to get SystemBeforeSnapshot") + return 1 + } + + b, err := json.MarshalIndent(brSnap.Msg.Snapshot.ToMap(), "", " ") + if err != nil { + log.WithContext(ctx).WithFields(log.Fields{ + "change-uuid": changeUuid, + "system-before-uuid": sbsUuid.String(), + }).Errorf("Error rendering system before snapshot: %v", err) + return 1 + } + + err = printJson(ctx, b, "system-before", sbsUuid.String()) + if err != nil { + return 1 + } + } + } + + sasUuid := uuid.UUID(change.Properties.SystemAfterSnapshotUUID) + if sasUuid != uuid.Nil { + brSnap, err := snapshots.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{ + Msg: &sdp.GetSnapshotRequest{ + UUID: sasUuid[:], + }, + }) + // continue processing if item not found + if connect.CodeOf(err) != connect.CodeNotFound { + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "change-uuid": changeUuid, + "system-after-uuid": sasUuid.String(), + }).Error("failed to get SystemAfterSnapshot") + return 1 + } + + b, err := json.MarshalIndent(brSnap.Msg.Snapshot.ToMap(), "", " ") + if err != nil { + log.WithContext(ctx).WithFields(log.Fields{ + "change-uuid": changeUuid, + "system-after-uuid": sasUuid.String(), + }).Errorf("Error rendering system after snapshot: %v", err) + return 1 + } + + err = printJson(ctx, b, "system-after", sasUuid.String()) + if err != nil { + return 1 + } + } + } + } + } + + return 0 +} + +func printJson(ctx context.Context, b []byte, prefix, id string) error { + switch viper.GetString("format") { + case "json": + fmt.Println(string(b)) + case "files": + dir := viper.GetString("dir") + if dir == "" { + return errors.New("need --dir value to write to files") + } + + // write the change to a file + fileName := fmt.Sprintf("%v/%v-%v.json", dir, prefix, id) + file, err := os.Create(fileName) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "prefix": prefix, + "id": id, + "output-dir": dir, + "output-file": fileName, + }).Error("failed to create file") + return err + } + + _, err = file.Write(b) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "prefix": prefix, + "id": id, + "output-dir": dir, + "output-file": fileName, + }).Error("failed to write file") + return err + } + } + + return nil +} + +func init() { + rootCmd.AddCommand(listChangesCmd) + + listChangesCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") + listChangesCmd.PersistentFlags().String("format", "files", "How to render the change. Possible values: files, json") + listChangesCmd.PersistentFlags().String("dir", "./output", "A directory name to use for rendering changes when using the 'files' format") + listChangesCmd.PersistentFlags().Bool("fetch-data", false, "also fetch the blast radius and system state snapshots for each change") + + listChangesCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") +} From 151a6b8af86eeefbb27cad781ee599eb3b244c0a Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 13 Oct 2023 14:10:07 +0200 Subject: [PATCH 2/3] Stop processing token update when state mismatch is detected --- cmd/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/root.go b/cmd/root.go index f1d2c6a2..fb5269d4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -239,6 +239,7 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context, if state != oAuthStateString { log.WithContext(ctx).Errorf("Invalid state, expected %v, got %v", oAuthStateString, state) + return } // Exchange will do the handshake to retrieve the initial access token. From 835de5bd4b2b76510ca25c71cf3d3093b215da83 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 13 Oct 2023 14:10:23 +0200 Subject: [PATCH 3/3] Improved logging --- cmd/root.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index fb5269d4..dec32ab4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -328,6 +328,8 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context, if err != nil { log.WithContext(ctx).WithError(err).Errorf("Failed to encode token file at %v", path) } + + log.WithContext(ctx).Debugf("Saved token to %v", path) } // Set the token