diff --git a/assets/change.png b/assets/change.png
new file mode 100644
index 00000000..670f275c
Binary files /dev/null and b/assets/change.png differ
diff --git a/assets/changed.png b/assets/changed.png
new file mode 100644
index 00000000..3fa9725b
Binary files /dev/null and b/assets/changed.png differ
diff --git a/assets/created.png b/assets/created.png
new file mode 100644
index 00000000..449c7e58
Binary files /dev/null and b/assets/created.png differ
diff --git a/assets/deleted.png b/assets/deleted.png
new file mode 100644
index 00000000..8b17b749
Binary files /dev/null and b/assets/deleted.png differ
diff --git a/assets/edge.png b/assets/edge.png
new file mode 100644
index 00000000..75eb53ef
Binary files /dev/null and b/assets/edge.png differ
diff --git a/assets/high.png b/assets/high.png
new file mode 100644
index 00000000..9d156759
Binary files /dev/null and b/assets/high.png differ
diff --git a/assets/item.png b/assets/item.png
new file mode 100644
index 00000000..7d6c8b6a
Binary files /dev/null and b/assets/item.png differ
diff --git a/assets/low.png b/assets/low.png
new file mode 100644
index 00000000..647ab199
Binary files /dev/null and b/assets/low.png differ
diff --git a/assets/medium.png b/assets/medium.png
new file mode 100644
index 00000000..f9a36c80
Binary files /dev/null and b/assets/medium.png differ
diff --git a/assets/replaced.png b/assets/replaced.png
new file mode 100644
index 00000000..1840bb67
Binary files /dev/null and b/assets/replaced.png differ
diff --git a/assets/risks.png b/assets/risks.png
new file mode 100644
index 00000000..d5997340
Binary files /dev/null and b/assets/risks.png differ
diff --git a/cmd/comment.md b/cmd/comment.md
new file mode 100644
index 00000000..b47c1e28
--- /dev/null
+++ b/cmd/comment.md
@@ -0,0 +1,62 @@
+#
Expected Changes
+
+{{range .ExpectedChanges }}
+
+
{{ .Type }} › {{ .Title }}
+
+{{if .Diff }}
+```diff
+{{ .Diff }}
+```
+{{else}}
+(no changed attributes)
+{{end}}
+
+{{else}}
+No expected changes found.
+{{end}}
+
+
+##
Unmapped Changes
+
+> [!NOTE]
+> These changes couldn't be mapped to a real cloud resource and therefore won't be included in the blast radius calculation.
+
+
+{{range .UnmappedChanges }}
+
+
{{ .Type }} › {{ .Title }}
+
+{{if .Diff }}
+```diff
+{{ .Diff }}
+```
+{{else}}
+(no changed attributes)
+{{end}}
+
+{{else}}
+No unmapped changes found.
+{{end}}
+
+
+
+# Blast Radius
+
+|
Items |
Edges |
+|---|---|
+| 75 | 97
+
+[Open in Overmind]({{ .ChangeUrl }})
+
+
+
+{{if .Risks }}
+#
Risks
+
+{{range .Risks }}
+##
Impact on Target Groups [High]
+
+The various target groups including \"944651592624.eu-west-2.elbv2-target-group.k8s-default-nats-4650f3a363\", \"944651592624.eu-west-2.elbv2-target-group.k8s-default-smartloo-fd2416d9f8\", etc., that work alongside the load balancer for traffic routing may be indirectly affected if the security group change causes networking issues. This is especially important if these target groups rely on different ports other than 8080 for health checks or for directing incoming requests to backend services.
+{{end}}
+{{end}}
diff --git a/cmd/getchange.go b/cmd/getchange.go
index 7a693dc1..1d373ca3 100644
--- a/cmd/getchange.go
+++ b/cmd/getchange.go
@@ -2,15 +2,20 @@ package cmd
import (
"context"
+ _ "embed"
"encoding/json"
"fmt"
"os"
"os/signal"
"syscall"
+ "text/template"
"time"
"connectrpc.com/connect"
"github.com/google/uuid"
+ "github.com/hexops/gotextdiff"
+ "github.com/hexops/gotextdiff/myers"
+ diffspan "github.com/hexops/gotextdiff/span"
"github.com/overmindtech/ovm-cli/tracing"
"github.com/overmindtech/sdp-go"
log "github.com/sirupsen/logrus"
@@ -18,8 +23,12 @@ import (
"github.com/spf13/viper"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
+ "gopkg.in/yaml.v3"
)
+//go:embed comment.md
+var commentTemplate string
+
// getChangeCmd represents the get-change command
var getChangeCmd = &cobra.Command{
Use: "get-change {--uuid ID | --change https://app.overmind.tech/changes/c772d072-6b0b-4763-b7c5-ff5069beed4c}",
@@ -87,7 +96,7 @@ func GetChange(ctx context.Context, ready chan bool) int {
lf["uuid"] = changeUuid.String()
client := AuthenticatedChangesClient(ctx)
- response, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{
+ changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{
Msg: &sdp.GetChangeRequest{
UUID: changeUuid[:],
},
@@ -99,44 +108,187 @@ func GetChange(ctx context.Context, ready chan bool) int {
return 1
}
log.WithContext(ctx).WithFields(log.Fields{
- "change-uuid": uuid.UUID(response.Msg.Change.Metadata.UUID),
- "change-created": response.Msg.Change.Metadata.CreatedAt.AsTime(),
- "change-status": response.Msg.Change.Metadata.Status.String(),
- "change-name": response.Msg.Change.Properties.Title,
- "change-description": response.Msg.Change.Properties.Description,
+ "change-uuid": uuid.UUID(changeRes.Msg.Change.Metadata.UUID),
+ "change-created": changeRes.Msg.Change.Metadata.CreatedAt.AsTime(),
+ "change-status": changeRes.Msg.Change.Metadata.Status.String(),
+ "change-name": changeRes.Msg.Change.Properties.Title,
+ "change-description": changeRes.Msg.Change.Properties.Description,
}).Info("found change")
+ // diffRes, err := client.GetDiff(ctx, &connect.Request[sdp.GetDiffRequest]{
+ // Msg: &sdp.GetDiffRequest{
+ // ChangeUUID: changeUuid[:],
+ // },
+ // })
+ // if err != nil {
+ // log.WithContext(ctx).WithError(err).WithFields(log.Fields{
+ // "change-url": viper.GetString("change-url"),
+ // }).Error("failed to get change diff")
+ // return 1
+ // }
+ // log.WithContext(ctx).WithFields(log.Fields{
+ // "change-uuid": uuid.UUID(changeRes.Msg.Change.Metadata.UUID),
+ // }).Info("loaded change diff")
+
switch viper.GetString("format") {
case "json":
- b, err := json.MarshalIndent(response.Msg.Change.ToMap(), "", " ")
+ b, err := json.MarshalIndent(changeRes.Msg.Change.ToMap(), "", " ")
if err != nil {
- log.Errorf("Error rendering change: %v", err)
+ log.WithContext(ctx).WithField("input", fmt.Sprintf("%#v", changeRes.Msg.Change.ToMap())).WithError(err).Error("Error rendering change")
return 1
}
fmt.Println(string(b))
case "markdown":
- changeUrl := fmt.Sprintf("%v/changes/%v", viper.GetString("frontend"), changeUuid.String())
- if response.Msg.Change.Metadata.NumAffectedApps != 0 || response.Msg.Change.Metadata.NumAffectedItems != 0 {
- // we have affected stuff
- fmt.Printf(`## Blast Radius · [View in Overmind](%v)
+ type TemplateItem struct {
+ StatusAlt string
+ StatusIcon string
+ Type string
+ Title string
+ Diff string
+ }
+ type TemplateRisk struct {
+ SeverityAlt string
+ SeverityIcon string
+ SeverityText string
+ Title string
+ Description string
+ }
+ type TemplateData struct {
+ ChangeUrl string
+ ExpectedChanges []TemplateItem
+ UnmappedChanges []TemplateItem
+ BlastItems int
+ BlastEdges int
+ Risks []TemplateRisk
+ }
+ status := map[sdp.ItemDiffStatus]TemplateItem{
+ sdp.ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED: {
+ StatusAlt: "unspecified",
+ StatusIcon: "",
+ },
+ sdp.ItemDiffStatus_ITEM_DIFF_STATUS_UNCHANGED: {
+ StatusAlt: "unchanged",
+ StatusIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/item.png",
+ },
+ sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED: {
+ StatusAlt: "created",
+ StatusIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/created.png",
+ },
+ sdp.ItemDiffStatus_ITEM_DIFF_STATUS_UPDATED: {
+ StatusAlt: "updated",
+ StatusIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/changed.png",
+ },
+ sdp.ItemDiffStatus_ITEM_DIFF_STATUS_DELETED: {
+ StatusAlt: "deleted",
+ StatusIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/deleted.png",
+ },
+ sdp.ItemDiffStatus_ITEM_DIFF_STATUS_REPLACED: {
+ StatusAlt: "replaced",
+ StatusIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/replaced.png",
+ },
+ }
-> **Warning**
-> Overmind identified potentially affected apps and items as a result of this pull request.
+ severity := map[sdp.Risk_Severity]TemplateRisk{
+ sdp.Risk_SEVERITY_UNSPECIFIED: {
+ SeverityAlt: "unspecified",
+ SeverityIcon: "",
+ SeverityText: "unspecified",
+ },
+ sdp.Risk_SEVERITY_LOW: {
+ SeverityAlt: "low",
+ SeverityIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/low.png",
+ SeverityText: "Low",
+ },
+ sdp.Risk_SEVERITY_MEDIUM: {
+ SeverityAlt: "medium",
+ SeverityIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/medium.png",
+ SeverityText: "Medium",
+ },
+ sdp.Risk_SEVERITY_HIGH: {
+ SeverityAlt: "high",
+ SeverityIcon: "https://raw.githubusercontent.com/overmindtech/ovm-cli/ac4feb1b9dd73b5c42c5a515d12517b551d2886b/assets/high.png",
+ SeverityText: "High",
+ },
+ }
+ data := TemplateData{
+ ChangeUrl: fmt.Sprintf("%v/changes/%v", viper.GetString("frontend"), changeUuid.String()),
+ ExpectedChanges: []TemplateItem{},
+ UnmappedChanges: []TemplateItem{},
+ BlastItems: 75,
+ BlastEdges: 97,
+ Risks: []TemplateRisk{},
+ }
-
+ for _, item := range changeRes.Msg.Change.Properties.PlannedChanges {
+ var before, after string
+ if item.Before != nil {
+ bb, err := yaml.Marshal(item.Before.Attributes.AttrStruct.AsMap())
+ if err != nil {
+ log.WithContext(ctx).WithError(err).Error("error marshalling 'before' attributes")
+ before = ""
+ } else {
+ before = string(bb)
+ }
+ }
+ if item.After != nil {
+ ab, err := yaml.Marshal(item.After.Attributes.AttrStruct.AsMap())
+ if err != nil {
+ log.WithContext(ctx).WithError(err).Error("error marshalling 'after' attributes")
+ after = ""
+ } else {
+ after = string(ab)
+ }
+ }
+ edits := myers.ComputeEdits(diffspan.URIFromPath("current"), before, after)
+ diff := fmt.Sprint(gotextdiff.ToUnified("current", "planned", before, edits))
-|
Affected items |
-| -------------- |
-| [%v items](%v) |
-`, changeUrl, response.Msg.Change.Metadata.NumAffectedItems, changeUrl)
- } else {
- fmt.Printf(`## Blast Radius · [View in Overmind](%v)
+ if item.Item != nil {
+ data.ExpectedChanges = append(data.ExpectedChanges, TemplateItem{
+ StatusAlt: status[item.Status].StatusAlt,
+ StatusIcon: status[item.Status].StatusIcon,
+ Type: item.Item.Type,
+ Title: item.Item.UniqueAttributeValue,
+ Diff: diff,
+ })
+ } else {
+ var typ, title string
+ if item.After != nil {
+ typ = item.After.Type
+ title = item.After.UniqueAttributeValue()
+ } else if item.Before != nil {
+ typ = item.Before.Type
+ title = item.Before.UniqueAttributeValue()
+ }
+ data.UnmappedChanges = append(data.ExpectedChanges, TemplateItem{
+ StatusAlt: status[item.Status].StatusAlt,
+ StatusIcon: status[item.Status].StatusIcon,
+ Type: typ,
+ Title: title,
+ Diff: diff,
+ })
+ }
+ }
-> **✅ Checks complete**
-> Overmind didn't identify any potentially affected apps and items as a result of this pull request.
+ for _, risk := range changeRes.Msg.Change.Metadata.Risks {
+ data.Risks = append(data.Risks, TemplateRisk{
+ SeverityAlt: severity[risk.Severity].SeverityAlt,
+ SeverityIcon: severity[risk.Severity].SeverityIcon,
+ SeverityText: severity[risk.Severity].SeverityText,
+ Title: risk.Title,
+ Description: risk.Description,
+ })
+ }
-`, changeUrl)
+ tmpl, err := template.New("comment").Parse(commentTemplate)
+ if err != nil {
+ log.WithContext(ctx).WithError(err).Error("error parsing comment template")
+ return 1
+ }
+ err = tmpl.Execute(os.Stdout, data)
+ if err != nil {
+ log.WithContext(ctx).WithField("input", fmt.Sprintf("%#v", data)).WithError(err).Error("error rendering comment")
+ return 1
}
}
diff --git a/go.mod b/go.mod
index 2b1d3969..c1f61dd3 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
connectrpc.com/connect v1.12.0
github.com/getsentry/sentry-go v0.25.0
github.com/google/uuid v1.4.0
+ github.com/hexops/gotextdiff v1.0.3
github.com/jedib0t/go-pretty/v6 v6.4.9
github.com/mattn/go-isatty v0.0.20
github.com/overmindtech/sdp-go v0.57.0
@@ -26,6 +27,7 @@ require (
go.opentelemetry.io/otel/trace v1.19.0
golang.org/x/oauth2 v0.14.0
google.golang.org/protobuf v1.31.0
+ gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -76,6 +78,5 @@ require (
google.golang.org/grpc v1.58.3 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
nhooyr.io/websocket v1.8.10 // indirect
)
diff --git a/go.sum b/go.sum
index df3e4d09..3b5963c9 100644
--- a/go.sum
+++ b/go.sum
@@ -153,6 +153,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=