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 @@ +# mapped Expected Changes + +{{range .ExpectedChanges }} +
+{{ .StatusAlt }} {{ .Type }} › {{ .Title }} + +{{if .Diff }} +```diff +{{ .Diff }} +``` +{{else}} +(no changed attributes) +{{end}} +
+{{else}} +No expected changes found. +{{end}} + + +## unmapped 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 }} +
+{{ .StatusAlt }} {{ .Type }} › {{ .Title }} + +{{if .Diff }} +```diff +{{ .Diff }} +``` +{{else}} +(no changed attributes) +{{end}} +
+{{else}} +No unmapped changes found. +{{end}} + + + +# Blast Radius + +| mapped Items | edge Edges | +|---|---| +| 75 | 97 + +[Open in Overmind]({{ .ChangeUrl }}) + + + +{{if .Risks }} +# warning Risks + +{{range .Risks }} +## {{ .SeverityAlt }} 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) chain link icon + 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)) -| icon for blast radius items Affected items | -| -------------- | -| [%v items](%v) | -`, changeUrl, response.Msg.Change.Metadata.NumAffectedItems, changeUrl) - } else { - fmt.Printf(`## Blast Radius   ·   [View in Overmind](%v) chain link icon + 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=