Skip to content

Commit

Permalink
Merge pull request #93 from overmindtech/list-changes
Browse files Browse the repository at this point in the history
ovm-cli list-changes downloads metadata or everything for an account
  • Loading branch information
dylanratcliffe authored Oct 13, 2023
2 parents 4d57cb6 + 835de5b commit cad30e4
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ go.work
dist/
tracing/commit.txt
cmd/commit.txt
output
298 changes: 298 additions & 0 deletions cmd/listchanges.go
Original file line number Diff line number Diff line change
@@ -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")
}
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -327,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
Expand Down

0 comments on commit cad30e4

Please sign in to comment.