Skip to content

Commit a91e26b

Browse files
committed
ovm-cli list-changes downloads metadata or everything for an account's changes
1 parent 4d57cb6 commit a91e26b

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ go.work
2323
dist/
2424
tracing/commit.txt
2525
cmd/commit.txt
26+
output

cmd/listchanges.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
13+
"github.com/bufbuild/connect-go"
14+
"github.com/google/uuid"
15+
"github.com/overmindtech/ovm-cli/tracing"
16+
"github.com/overmindtech/sdp-go"
17+
log "github.com/sirupsen/logrus"
18+
"github.com/spf13/cobra"
19+
"github.com/spf13/viper"
20+
"go.opentelemetry.io/otel/attribute"
21+
"go.opentelemetry.io/otel/trace"
22+
)
23+
24+
// listChangesCmd represents the get-change command
25+
var listChangesCmd = &cobra.Command{
26+
Use: "list-changes --dir ./output",
27+
Short: "Displays the contents of a change.",
28+
PreRun: func(cmd *cobra.Command, args []string) {
29+
// Bind these to viper
30+
err := viper.BindPFlags(cmd.Flags())
31+
if err != nil {
32+
log.WithError(err).Fatal("could not bind `get-change` flags")
33+
}
34+
},
35+
Run: func(cmd *cobra.Command, args []string) {
36+
sigs := make(chan os.Signal, 1)
37+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
38+
39+
ctx, cancel := context.WithCancel(context.Background())
40+
defer cancel()
41+
42+
// Create a goroutine to watch for cancellation signals
43+
go func() {
44+
select {
45+
case <-sigs:
46+
cancel()
47+
case <-ctx.Done():
48+
}
49+
}()
50+
51+
exitcode := ListChanges(ctx, nil)
52+
tracing.ShutdownTracer()
53+
os.Exit(exitcode)
54+
},
55+
}
56+
57+
func ListChanges(ctx context.Context, ready chan bool) int {
58+
timeout, err := time.ParseDuration(viper.GetString("timeout"))
59+
if err != nil {
60+
log.Errorf("invalid --timeout value '%v', error: %v", viper.GetString("timeout"), err)
61+
return 1
62+
}
63+
64+
ctx, span := tracing.Tracer().Start(ctx, "CLI ListChanges", trace.WithAttributes(
65+
attribute.String("om.config", fmt.Sprintf("%v", viper.AllSettings())),
66+
))
67+
defer span.End()
68+
69+
ctx, err = ensureToken(ctx, []string{"changes:read"})
70+
if err != nil {
71+
log.WithContext(ctx).WithFields(log.Fields{
72+
"url": viper.GetString("url"),
73+
}).WithError(err).Error("failed to authenticate")
74+
return 1
75+
}
76+
77+
// apply a timeout to the main body of processing
78+
ctx, cancel := context.WithTimeout(ctx, timeout)
79+
defer cancel()
80+
81+
snapshots := AuthenticatedSnapshotsClient(ctx)
82+
bookmarks := AuthenticatedBookmarkClient(ctx)
83+
changes := AuthenticatedChangesClient(ctx)
84+
85+
response, err := changes.ListChanges(ctx, &connect.Request[sdp.ListChangesRequest]{
86+
Msg: &sdp.ListChangesRequest{},
87+
})
88+
if err != nil {
89+
log.WithContext(ctx).WithError(err).Error("failed to list changes")
90+
return 1
91+
}
92+
for _, change := range response.Msg.Changes {
93+
changeUuid := uuid.UUID(change.Metadata.UUID)
94+
log.WithContext(ctx).WithFields(log.Fields{
95+
"change-uuid": changeUuid,
96+
"change-created": change.Metadata.CreatedAt.AsTime(),
97+
"change-status": change.Metadata.Status.String(),
98+
"change-name": change.Properties.Title,
99+
"change-description": change.Properties.Description,
100+
}).Info("found change")
101+
102+
b, err := json.MarshalIndent(change.ToMap(), "", " ")
103+
if err != nil {
104+
log.WithContext(ctx).Errorf("Error rendering change: %v", err)
105+
return 1
106+
}
107+
108+
err = printJson(ctx, b, "change", changeUuid.String())
109+
if err != nil {
110+
return 1
111+
}
112+
113+
if viper.GetBool("fetch-data") {
114+
ciUuid := uuid.UUID(change.Properties.ChangingItemsBookmarkUUID)
115+
if ciUuid != uuid.Nil {
116+
changingItems, err := bookmarks.GetBookmark(ctx, &connect.Request[sdp.GetBookmarkRequest]{
117+
Msg: &sdp.GetBookmarkRequest{
118+
UUID: ciUuid[:],
119+
},
120+
})
121+
// continue processing if item not found
122+
if connect.CodeOf(err) != connect.CodeNotFound {
123+
if err != nil {
124+
log.WithContext(ctx).WithError(err).WithFields(log.Fields{
125+
"change-uuid": changeUuid,
126+
"changing-items-uuid": ciUuid.String(),
127+
}).Error("failed to get ChangingItemsBookmark")
128+
return 1
129+
}
130+
131+
b, err := json.MarshalIndent(changingItems.Msg.Bookmark.ToMap(), "", " ")
132+
if err != nil {
133+
log.WithContext(ctx).WithFields(log.Fields{
134+
"change-uuid": changeUuid,
135+
"changing-items-uuid": ciUuid.String(),
136+
}).Errorf("Error rendering changing items bookmark: %v", err)
137+
return 1
138+
}
139+
140+
err = printJson(ctx, b, "changing-items", ciUuid.String())
141+
if err != nil {
142+
return 1
143+
}
144+
}
145+
}
146+
147+
brUuid := uuid.UUID(change.Properties.BlastRadiusSnapshotUUID)
148+
if brUuid != uuid.Nil {
149+
brSnap, err := snapshots.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{
150+
Msg: &sdp.GetSnapshotRequest{
151+
UUID: brUuid[:],
152+
},
153+
})
154+
// continue processing if item not found
155+
if connect.CodeOf(err) != connect.CodeNotFound {
156+
if err != nil {
157+
log.WithContext(ctx).WithError(err).WithFields(log.Fields{
158+
"change-uuid": changeUuid,
159+
"blast-radius-uuid": brUuid.String(),
160+
}).Error("failed to get BlastRadiusSnapshot")
161+
return 1
162+
}
163+
164+
b, err := json.MarshalIndent(brSnap.Msg.Snapshot.ToMap(), "", " ")
165+
if err != nil {
166+
log.WithContext(ctx).WithFields(log.Fields{
167+
"change-uuid": changeUuid,
168+
"blast-radius-uuid": brUuid.String(),
169+
}).Errorf("Error rendering blast radius snapshot: %v", err)
170+
return 1
171+
}
172+
173+
err = printJson(ctx, b, "blast-radius", brUuid.String())
174+
if err != nil {
175+
return 1
176+
}
177+
}
178+
}
179+
180+
sbsUuid := uuid.UUID(change.Properties.SystemBeforeSnapshotUUID)
181+
if sbsUuid != uuid.Nil {
182+
brSnap, err := snapshots.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{
183+
Msg: &sdp.GetSnapshotRequest{
184+
UUID: sbsUuid[:],
185+
},
186+
})
187+
// continue processing if item not found
188+
if connect.CodeOf(err) != connect.CodeNotFound {
189+
if err != nil {
190+
log.WithContext(ctx).WithError(err).WithFields(log.Fields{
191+
"change-uuid": changeUuid,
192+
"system-before-uuid": sbsUuid.String(),
193+
}).Error("failed to get SystemBeforeSnapshot")
194+
return 1
195+
}
196+
197+
b, err := json.MarshalIndent(brSnap.Msg.Snapshot.ToMap(), "", " ")
198+
if err != nil {
199+
log.WithContext(ctx).WithFields(log.Fields{
200+
"change-uuid": changeUuid,
201+
"system-before-uuid": sbsUuid.String(),
202+
}).Errorf("Error rendering system before snapshot: %v", err)
203+
return 1
204+
}
205+
206+
err = printJson(ctx, b, "system-before", sbsUuid.String())
207+
if err != nil {
208+
return 1
209+
}
210+
}
211+
}
212+
213+
sasUuid := uuid.UUID(change.Properties.SystemAfterSnapshotUUID)
214+
if sasUuid != uuid.Nil {
215+
brSnap, err := snapshots.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{
216+
Msg: &sdp.GetSnapshotRequest{
217+
UUID: sasUuid[:],
218+
},
219+
})
220+
// continue processing if item not found
221+
if connect.CodeOf(err) != connect.CodeNotFound {
222+
if err != nil {
223+
log.WithContext(ctx).WithError(err).WithFields(log.Fields{
224+
"change-uuid": changeUuid,
225+
"system-after-uuid": sasUuid.String(),
226+
}).Error("failed to get SystemAfterSnapshot")
227+
return 1
228+
}
229+
230+
b, err := json.MarshalIndent(brSnap.Msg.Snapshot.ToMap(), "", " ")
231+
if err != nil {
232+
log.WithContext(ctx).WithFields(log.Fields{
233+
"change-uuid": changeUuid,
234+
"system-after-uuid": sasUuid.String(),
235+
}).Errorf("Error rendering system after snapshot: %v", err)
236+
return 1
237+
}
238+
239+
err = printJson(ctx, b, "system-after", sasUuid.String())
240+
if err != nil {
241+
return 1
242+
}
243+
}
244+
}
245+
}
246+
}
247+
248+
return 0
249+
}
250+
251+
func printJson(ctx context.Context, b []byte, prefix, id string) error {
252+
switch viper.GetString("format") {
253+
case "json":
254+
fmt.Println(string(b))
255+
case "files":
256+
dir := viper.GetString("dir")
257+
if dir == "" {
258+
return errors.New("need --dir value to write to files")
259+
}
260+
261+
// write the change to a file
262+
fileName := fmt.Sprintf("%v/%v-%v.json", dir, prefix, id)
263+
file, err := os.Create(fileName)
264+
if err != nil {
265+
log.WithError(err).WithFields(log.Fields{
266+
"prefix": prefix,
267+
"id": id,
268+
"output-dir": dir,
269+
"output-file": fileName,
270+
}).Error("failed to create file")
271+
return err
272+
}
273+
274+
_, err = file.Write(b)
275+
if err != nil {
276+
log.WithError(err).WithFields(log.Fields{
277+
"prefix": prefix,
278+
"id": id,
279+
"output-dir": dir,
280+
"output-file": fileName,
281+
}).Error("failed to write file")
282+
return err
283+
}
284+
}
285+
286+
return nil
287+
}
288+
289+
func init() {
290+
rootCmd.AddCommand(listChangesCmd)
291+
292+
listChangesCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL")
293+
listChangesCmd.PersistentFlags().String("format", "files", "How to render the change. Possible values: files, json")
294+
listChangesCmd.PersistentFlags().String("dir", "./output", "A directory name to use for rendering changes when using the 'files' format")
295+
listChangesCmd.PersistentFlags().Bool("fetch-data", false, "also fetch the blast radius and system state snapshots for each change")
296+
297+
listChangesCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses")
298+
}

0 commit comments

Comments
 (0)