diff --git a/cmd/logging.go b/cmd/logging.go new file mode 100644 index 00000000..5d3a5727 --- /dev/null +++ b/cmd/logging.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "os" + + "github.com/mattn/go-isatty" + "github.com/ttacon/chalk" +) + +var tty bool + +func init() { + // Detect if we're in a TTY or not + tty = isatty.IsTerminal(os.Stdout.Fd()) +} + +var ( + // Styles + Underline = TextStyle{chalk.Underline} + Bold = TextStyle{chalk.Bold} + + // Colors + Black = Color{chalk.Black} + Red = Color{chalk.Red} + Green = Color{chalk.Green} + Yellow = Color{chalk.Yellow} + Blue = Color{chalk.Blue} + Magenta = Color{chalk.Magenta} + Cyan = Color{chalk.Cyan} + White = Color{chalk.White} +) + +// A type that wraps chalk.TextStyle but adds detections for if we're in a TTY +type TextStyle struct { + underlying chalk.TextStyle +} + +func (t TextStyle) TextStyle(val string) string { + if !tty { + // Don't style if we're not in a TTY + return val + } + + return t.underlying.TextStyle(val) +} + +// A type that wraps chalk.Color but adds detections for if we're in a TTY +type Color struct { + underlying chalk.Color +} + +func (c Color) Color(val string) string { + if !tty { + // Don't style if we're not in a TTY + return val + } + + return c.underlying.Color(val) +} diff --git a/cmd/root.go b/cmd/root.go index 86fbda77..ba8927b3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -134,7 +134,7 @@ func ensureToken(ctx context.Context, requiredScopes []string, signals chan os.S log.WithContext(ctx).Error(err) return } - log.WithContext(ctx).Debug("Got token 1!") + log.WithContext(ctx).Debug("Got token") tokenChan <- tok @@ -148,7 +148,7 @@ func ensureToken(ctx context.Context, requiredScopes []string, signals chan os.S u := config.AuthCodeURL(oAuthStateString, oauth2.AccessTypeOnline, audienceOption) - log.WithContext(ctx).Infof("Log in here: %v", u) + log.WithContext(ctx).Infof("Follow this link to authenticate: %v", Underline.TextStyle(u)) // Start the webserver log.WithContext(ctx).Trace("Starting webserver to listen for callback, press Ctrl+C to cancel") @@ -166,7 +166,7 @@ func ensureToken(ctx context.Context, requiredScopes []string, signals chan os.S var token *oauth2.Token select { case token = <-tokenChan: - log.WithContext(ctx).Debug("Got token 2!") + // Keep working case <-signals: log.WithContext(ctx).Debug("Received interrupt, exiting") return ctx, errors.New("cancelled") @@ -175,9 +175,11 @@ func ensureToken(ctx context.Context, requiredScopes []string, signals chan os.S // Stop the server err = srv.Shutdown(ctx) if err != nil { - log.WithContext(ctx).WithError(err).Info("failed to shutdown auth callback server, but continuing anyways") + log.WithContext(ctx).WithError(err).Warn("failed to shutdown auth callback server, but continuing anyway") } + log.WithContext(ctx).Info("Authenticated successfully ✅") + // Set the token return context.WithValue(ctx, sdp.UserTokenContextKey{}, token.AccessToken), nil } @@ -277,6 +279,10 @@ func init() { // Run this before we do anything to set up the loglevel rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + formatter := new(log.TextFormatter) + formatter.DisableTimestamp = true + log.SetFormatter(formatter) + // Read env vars var lvl log.Level @@ -290,7 +296,6 @@ func init() { lvl = log.InfoLevel } log.SetLevel(lvl) - log.WithField("level", lvl).Infof("set log level from config") if viper.GetBool("json-log") { log.SetFormatter(&log.JSONFormatter{}) @@ -317,11 +322,3 @@ func initConfig() { viper.SetEnvKeyReplacer(replacer) viper.AutomaticEnv() // read in environment variables that match } - -// must panics if the passed in error is not nil -// use this for init-time error checking of viper/cobra stuff that sometimes errors if the flag does not exist -func must(err error) { - if err != nil { - panic(fmt.Errorf("error initialising: %w", err)) - } -} diff --git a/cmd/submitplan.go b/cmd/submitplan.go index 00865d3f..52f0f6b7 100644 --- a/cmd/submitplan.go +++ b/cmd/submitplan.go @@ -57,9 +57,6 @@ var submitPlanCmd = &cobra.Command{ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - if viper.GetString("plan-json") != "" { - args = append(args, viper.GetString("plan-json")) - } exitcode := SubmitPlan(sigs, args, nil) tracing.ShutdownTracer() os.Exit(exitcode) @@ -72,14 +69,79 @@ type TfData struct { Values map[string]any } -func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fields) ([]*sdp.Query, error) { - var changing_items []*sdp.Query +type MappedPlan struct { + // Map of unsupported types and their changes + UnsupportedChanges map[string][]ResourceChange + + // Map of supported types and their mapped queries + SupportedChanges map[string][]TerraformToOvermindMapping +} + +func (m MappedPlan) NumUnsupportedChanges() int { + var num int + + for _, v := range m.UnsupportedChanges { + num += len(v) + } + + return num +} + +func (m MappedPlan) NumSupportedChanges() int { + var num int + + for _, v := range m.SupportedChanges { + num += len(v) + } + + return num +} + +func (m MappedPlan) Queries() []*sdp.Query { + queries := make([]*sdp.Query, 0) + + for _, mappings := range m.SupportedChanges { + for _, mapping := range mappings { + queries = append(queries, mapping.OvermindQuery) + } + } + + return queries +} + +func NewMappedPlan() *MappedPlan { + return &MappedPlan{ + UnsupportedChanges: make(map[string][]ResourceChange), + SupportedChanges: make(map[string][]TerraformToOvermindMapping), + } +} + +// Merges another mapped plan into this one +func (m *MappedPlan) Merge(other *MappedPlan) { + for k, v := range other.UnsupportedChanges { + m.UnsupportedChanges[k] = append(m.UnsupportedChanges[k], v...) + } + + for k, v := range other.SupportedChanges { + m.SupportedChanges[k] = append(m.SupportedChanges[k], v...) + } +} + +type TerraformToOvermindMapping struct { + TerraformResource *Resource + OvermindQuery *sdp.Query +} + +func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fields) (*MappedPlan, error) { + mappedPlan := NewMappedPlan() + + var overmindMappings []TerraformToOvermindMapping // read results from `terraform show -json ${tfplan file}` planJSON, err := os.ReadFile(fileName) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to read terraform file") - return changing_items, err + log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed to read terraform plan") + return nil, err } var plan Plan @@ -88,6 +150,9 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi return nil, fmt.Errorf("failed to parse %v: %w", fileName, err) } + // Track how many valid resource changes there are in the plan + numPlanResourceChanges := 0 + // for all managed resources: for _, resourceChange := range plan.ResourceChanges { if len(resourceChange.Change.Actions) == 0 || resourceChange.Change.Actions[0] == "no-op" { @@ -95,18 +160,21 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi continue } + numPlanResourceChanges++ + awsMappings := datamaps.AwssourceData[resourceChange.Type] k8sMappings := datamaps.K8ssourceData[resourceChange.Type] mappings := append(awsMappings, k8sMappings...) if len(mappings) == 0 { - log.WithContext(ctx).WithFields(lf).WithField("terraform-address", resourceChange.Address).Warn("skipping unmapped resource") + log.WithContext(ctx).WithFields(lf).WithField("terraform-address", resourceChange.Address).Debug("Skipping unmapped resource") continue } - var currentResource *Resource for _, mapData := range mappings { + var currentResource *Resource + // Look for the resource in the prior values first, since this is // the *previous* state we're like to be able to find it in the // actual infra @@ -123,7 +191,7 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi log.WithContext(ctx). WithFields(lf). WithField("terraform-address", resourceChange.Address). - WithField("terraform-query-field", mapData.QueryField).Warn("skipping resource without values") + WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without values") continue } @@ -132,7 +200,7 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi log.WithContext(ctx). WithFields(lf). WithField("terraform-address", resourceChange.Address). - WithField("terraform-query-field", mapData.QueryField).Warn("skipping resource without query field") + WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without query field") continue } @@ -149,7 +217,7 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi log.WithContext(ctx). WithFields(lf). WithField("terraform-address", resourceChange.Address). - Debug("skipping provider mapping for resource without config") + Debug("Skipping provider mapping for resource without config") } else { // Look up the provider config key in the mappings mappings := make(map[string]map[string]string) @@ -161,7 +229,7 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi WithFields(lf). WithField("terraform-address", resourceChange.Address). WithError(err). - Error("failed to parse overmind_mappings output") + Error("Failed to parse overmind_mappings output") } else { currentProviderMappings, ok := mappings[configResource.ProviderConfigKey] @@ -170,7 +238,7 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi WithFields(lf). WithField("terraform-address", resourceChange.Address). WithField("provider-config-key", configResource.ProviderConfigKey). - Info("found provider mappings") + Debug("Found provider mappings") // We have mappings for this provider, so set them // in the `provider_mapping` value @@ -184,7 +252,7 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi scope, err := InterpolateScope(mapData.Scope, dataMap) if err != nil { - log.WithContext(ctx).WithError(err).Infof("could not find scope mapping variables %v, adding them will result in better results. Error: ", mapData.Scope) + log.WithContext(ctx).WithError(err).Debugf("Could not find scope mapping variables %v, adding them will result in better results. Error: ", mapData.Scope) scope = "*" } @@ -198,18 +266,62 @@ func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fi UUID: u[:], } - changing_items = append(changing_items, &newQuery) + overmindMappings = append(overmindMappings, TerraformToOvermindMapping{ + TerraformResource: currentResource, + OvermindQuery: &newQuery, + }) log.WithContext(ctx).WithFields(log.Fields{ "scope": newQuery.Scope, "type": newQuery.Type, "query": newQuery.Query, "method": newQuery.Method.String(), - }).Debug("mapped terraform to query") + }).Debug("Mapped terraform to query") } } - return changing_items, nil + // Group plan changes by type, we will later delete the types that were + // mapped successfully + for _, resourceChange := range plan.ResourceChanges { + mappedPlan.UnsupportedChanges[resourceChange.Type] = append(mappedPlan.UnsupportedChanges[resourceChange.Type], resourceChange) + } + + // Group mapped items by type + for _, mapping := range overmindMappings { + mappedPlan.SupportedChanges[mapping.TerraformResource.Type] = append(mappedPlan.SupportedChanges[mapping.TerraformResource.Type], mapping) + // Delete supported type from unsupported map + delete(mappedPlan.UnsupportedChanges, mapping.TerraformResource.Type) + } + + resourceWord := "resource" + if len(overmindMappings) > 1 { + resourceWord = "resources" + } + + supported := "" + + if mappedPlan.NumSupportedChanges() > 0 { + supported = Green.Color(fmt.Sprintf("%v supported", mappedPlan.NumSupportedChanges())) + } + + unsupported := "" + + if mappedPlan.NumUnsupportedChanges() > 0 { + unsupported = Yellow.Color(fmt.Sprintf("%v unsupported", mappedPlan.NumUnsupportedChanges())) + } + + log.WithContext(ctx).Infof("Plan (%v) contained %v changing %v: %v %v", fileName, numPlanResourceChanges, resourceWord, supported, unsupported) + + // Log the types + for typ, mappings := range mappedPlan.SupportedChanges { + log.WithContext(ctx).Infof(Green.Color(" ✓ %v (%v)"), typ, len(mappings)) + } + + for typ, mappings := range mappedPlan.UnsupportedChanges { + log.WithContext(ctx).Infof(Yellow.Color(" ✗ %v (%v)"), typ, len(mappings)) + } + + return mappedPlan, nil } func changeTitle(arg string) string { @@ -238,7 +350,7 @@ func changeTitle(arg string) string { username = u.Username result := fmt.Sprintf("Deployment from %v by %v", describe, username) - log.WithField("generated-title", result).Infof("using default title") + log.WithField("generated-title", result).Debug("Using default title") return result } @@ -272,23 +384,30 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - queries := []*sdp.Query{} + fileWord := "file" + if len(files) > 1 { + fileWord = "files" + } + + log.WithContext(ctx).Infof("Reading %v plan %v", len(files), fileWord) + + planMappings := NewMappedPlan() + for _, f := range files { lf["file"] = f - log.WithContext(ctx).WithFields(lf).Info("resolving items from terraform plan") - q, err := changingItemQueriesFromPlan(ctx, f, lf) + mappings, err := changingItemQueriesFromPlan(ctx, f, lf) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(lf).Error("parse terraform plan") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("Error parsing terraform plan") return 1 } - queries = append(queries, q...) + planMappings.Merge(mappings) } delete(lf, "file") client := AuthenticatedChangesClient(ctx) changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to searching for existing changes") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed searching for existing changes") return 1 } @@ -306,32 +425,33 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { }, }) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to create change") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed to create change") return 1 } maybeChangeUuid := createResponse.Msg.Change.Metadata.GetUUIDParsed() if maybeChangeUuid == nil { - log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to read change id") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed to read change id") return 1 } changeUuid = *maybeChangeUuid lf["change"] = changeUuid - log.WithContext(ctx).WithFields(lf).Info("created a new change") + log.WithContext(ctx).WithFields(lf).Info("Created a new change") } else { lf["change"] = changeUuid - log.WithContext(ctx).WithFields(lf).Info("re-using change") + log.WithContext(ctx).WithFields(lf).Info("Re-using change") } - receivedItems := []*sdp.Reference{} + receivedItems := make([]*sdp.Item, 0) - if len(queries) > 0 { + if len(planMappings.Queries()) > 0 { options := &websocket.DialOptions{ HTTPClient: NewAuthenticatedClient(ctx, otelhttp.DefaultClient), } - log.WithContext(ctx).WithFields(lf).WithField("item_count", len(queries)).Info("identifying items") + log.WithContext(ctx).Infof("Finding expected changes in Overmind") + // nolint: bodyclose // nhooyr.io/websocket reads the body internally c, _, err := websocket.Dial(ctx, viper.GetString("gateway-url"), options) if err != nil { @@ -345,7 +465,7 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { queriesSentChan := make(chan struct{}) go func() { - for _, q := range queries { + for _, q := range planMappings.Queries() { req := sdp.GatewayRequest{ MinStatusInterval: minStatusInterval, RequestType: &sdp.GatewayRequest_Query{ @@ -386,7 +506,7 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { log.WithContext(ctx).WithFields(lf).WithFields(log.Fields{ "code": e.Code.String(), "reason": e.Reason, - }).Info("Websocket closing") + }).Debug("Websocket closing") return } log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to read response") @@ -398,6 +518,7 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { }() activeQueries := make(map[uuid.UUID]bool) + queryErrors := make(map[uuid.UUID][]*sdp.QueryError) queriesSent := false // Read the responses @@ -434,13 +555,13 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { allDone := allDone(ctx, activeQueries, lf) statusFields["allDone"] = allDone if allDone && queriesSent { - log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Info("all responders and queries done") + log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Info("All responders and queries done") break responses } else { - log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Info("all responders done, with unfinished queries") + log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Info("All responders done, with unfinished queries") } } else { - log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Info("still waiting for responders") + log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Debug("Still waiting for responders") } case *sdp.GatewayResponse_QueryStatus: @@ -450,7 +571,7 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { } queryUuid := status.GetUUIDParsed() if queryUuid == nil { - log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Debugf("Received QueryStatus with nil UUID") + log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Debug("Received QueryStatus with nil UUID") continue responses } statusFields["query"] = queryUuid @@ -470,24 +591,30 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { statusFields["unexpected_status"] = true } - log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Debugf("query status update") + log.WithContext(ctx).WithFields(lf).WithFields(statusFields).Debug("Query status update") case *sdp.GatewayResponse_NewItem: item := resp.GetNewItem() - log.WithContext(ctx).WithFields(lf).WithField("item", item.GloballyUniqueName()).Infof("new item") + log.WithContext(ctx).WithFields(lf).WithField("item", item.GloballyUniqueName()).Debug("New item") - receivedItems = append(receivedItems, item.Reference()) + receivedItems = append(receivedItems, item) case *sdp.GatewayResponse_NewEdge: - log.WithContext(ctx).WithFields(lf).Debug("ignored edge") + log.WithContext(ctx).WithFields(lf).Debug("Ignored edge") case *sdp.GatewayResponse_QueryError: err := resp.GetQueryError() - log.WithContext(ctx).WithFields(lf).WithError(err).Errorf("Error from %v(%v)", err.ResponderName, err.SourceName) + uuid := err.GetUUIDParsed() + + if uuid != nil { + queryErrors[*uuid] = append(queryErrors[*uuid], err) + } + + log.WithContext(ctx).WithFields(lf).WithError(err).Debugf("Error from %v(%v)", err.ResponderName, err.SourceName) case *sdp.GatewayResponse_Error: err := resp.GetError() - log.WithContext(ctx).WithFields(lf).WithField(log.ErrorKey, err).Errorf("generic error") + log.WithContext(ctx).WithFields(lf).WithField(log.ErrorKey, err).Debug("Generic error") default: j := protojson.Format(resp) @@ -495,23 +622,68 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { } } } + + // Print a summary of the results so far. I would like for this to be + // nicer and do things like tell you why it failed, but for now this + // will have to do + for tfType, mappings := range planMappings.SupportedChanges { + log.WithContext(ctx).Infof(" %v", tfType) + + for _, mapping := range mappings { + queryUUID := mapping.OvermindQuery.ParseUuid() + + // Check for items matching this query UUID + found := false + + for _, item := range receivedItems { + if item.Metadata.SourceQuery.ParseUuid() == queryUUID { + found = true + } + } + + if found { + log.WithContext(ctx).Infof(Green.Color(" ✓ %v (found)"), mapping.TerraformResource.Name) + } else { + log.WithContext(ctx).Infof(Red.Color(" ✗ %v (not found)"), mapping.TerraformResource.Name) + + relatedErrors, found := queryErrors[queryUUID] + + if found { + for _, err := range relatedErrors { + log.WithContext(ctx).WithFields(log.Fields{ + "type": err.ErrorType, + "source": err.SourceName, + "responder": err.ResponderName, + }).Errorf(" %v", err.ErrorString) + } + } + } + } + } } else { - log.WithContext(ctx).WithFields(lf).Info("no item queries mapped, skipping changing items") + log.WithContext(ctx).WithFields(lf).Info("No item queries mapped, skipping changing items") } if len(receivedItems) > 0 { - log.WithContext(ctx).WithFields(lf).WithField("received_items", len(receivedItems)).Info("updating changing items on the change record") + log.WithContext(ctx).WithFields(lf).WithField("received_items", len(receivedItems)).Info("Updating changing items on the change record") } else { - log.WithContext(ctx).WithFields(lf).WithField("received_items", len(receivedItems)).Info("updating change record with no items") + log.WithContext(ctx).WithFields(lf).WithField("received_items", len(receivedItems)).Info("Updating change record with no items") } + + changingItemRefs := make([]*sdp.Reference, len(receivedItems)) + + for i, item := range receivedItems { + changingItemRefs[i] = item.Reference() + } + resultStream, err := client.UpdateChangingItems(ctx, &connect.Request[sdp.UpdateChangingItemsRequest]{ Msg: &sdp.UpdateChangingItemsRequest{ ChangeUUID: changeUuid[:], - ChangingItems: receivedItems, + ChangingItems: changingItemRefs, }, }) if err != nil { - log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to update changing items") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to update changing items") return 1 } @@ -524,18 +696,18 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { // to avoid spanning the cli output time_since_last_log := time.Since(last_log) if first_log || msg.State != sdp.CalculateBlastRadiusResponse_STATE_DISCOVERING || time_since_last_log > 250*time.Millisecond { - log.WithContext(ctx).WithFields(lf).WithField("msg", msg).Info("status update") + log.WithContext(ctx).WithFields(lf).WithField("msg", msg).Info("Status update") last_log = time.Now() first_log = false } } if resultStream.Err() != nil { - log.WithContext(ctx).WithFields(lf).WithError(resultStream.Err()).Error("error streaming results") + log.WithContext(ctx).WithFields(lf).WithError(resultStream.Err()).Error("Error streaming results") return 1 } changeUrl := fmt.Sprintf("%v/changes/%v", viper.GetString("frontend"), changeUuid) - log.WithContext(ctx).WithFields(lf).WithField("change-url", changeUrl).Info("change ready") + log.WithContext(ctx).WithFields(lf).WithField("change-url", changeUrl).Info("Change ready") fmt.Println(changeUrl) fetchResponse, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ @@ -544,21 +716,21 @@ func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int { }, }) if err != nil { - log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to get updated change") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to get updated change") return 1 } for _, a := range fetchResponse.Msg.Change.Properties.AffectedAppsUUID { appUuid, err := uuid.FromBytes(a) if err != nil { - log.WithContext(ctx).WithFields(lf).WithError(err).WithField("app", a).Error("received invalid app uuid") + log.WithContext(ctx).WithFields(lf).WithError(err).WithField("app", a).Error("Received invalid app uuid") continue } log.WithContext(ctx).WithFields(lf).WithFields(log.Fields{ "change-url": changeUrl, "app": appUuid, "app-url": fmt.Sprintf("%v/apps/%v", viper.GetString("frontend"), appUuid), - }).Info("affected app") + }).Info("Affected app") } return 0 @@ -568,7 +740,7 @@ func allDone(ctx context.Context, activeQueries map[uuid.UUID]bool, lf log.Field allDone := true for q := range activeQueries { if activeQueries[q] { - log.WithContext(ctx).WithFields(lf).WithField("query", q).Debugf("query still active") + log.WithContext(ctx).WithFields(lf).WithField("query", q).Debugf("Query still active") allDone = false break } @@ -582,9 +754,6 @@ func init() { submitPlanCmd.PersistentFlags().String("changes-url", "", "The changes service API endpoint (defaults to --url)") submitPlanCmd.PersistentFlags().String("frontend", "https://app.overmind.tech", "The frontend base URL") - submitPlanCmd.PersistentFlags().String("plan-json", "./tfplan.json", "Parse changing items from this terraform plan JSON file. Generate this using 'terraform show -json PLAN_FILE'") - must(submitPlanCmd.PersistentFlags().MarkHidden("plan-json")) // better suited by using `args` - submitPlanCmd.PersistentFlags().String("title", "", "Short title for this change. If this is not specified, ovm-cli will try to come up with one for you.") submitPlanCmd.PersistentFlags().String("description", "", "Quick description of the change.") submitPlanCmd.PersistentFlags().String("ticket-link", "*", "Link to the ticket for this change.") diff --git a/cmd/submitplan_test.go b/cmd/submitplan_test.go index e3388c13..f65fb288 100644 --- a/cmd/submitplan_test.go +++ b/cmd/submitplan_test.go @@ -8,51 +8,65 @@ import ( ) func TestChangingItemQueriesFromPlan(t *testing.T) { - queries, err := changingItemQueriesFromPlan(context.Background(), "testdata/plan.json", logrus.Fields{}) + mappedPlan, err := changingItemQueriesFromPlan(context.Background(), "testdata/plan.json", logrus.Fields{}) if err != nil { t.Error(err) } - if len(queries) != 3 { - t.Errorf("Expected 3 queries, got %v", len(queries)) + deployments, exists := mappedPlan.SupportedChanges["kubernetes_deployment"] + + if !exists { + t.Errorf("Expected kubernetes_deployment to be in supported changes") + } + + if len(deployments) != 2 { + t.Errorf("Expected 2 deployments, got %v", len(deployments)) + } + + if deployments[0].OvermindQuery.Type != "Deployment" { + t.Errorf("Expected query type to be Deployment, got %v", deployments[0].OvermindQuery.Type) + } + + if deployments[0].OvermindQuery.Query != "nats-box" { + t.Errorf("Expected query to be nats-box, got %v", deployments[0].OvermindQuery.Query) } - if queries[0].Type != "Deployment" { - t.Errorf("Expected query type to be Deployment, got %v", queries[0].Type) + if deployments[0].OvermindQuery.Scope != "*" { + t.Errorf("Expected query scope to be *, got %v", deployments[0].OvermindQuery.Scope) } - if queries[0].Query != "nats-box" { - t.Errorf("Expected query to be nats-box, got %v", queries[0].Query) + if deployments[1].OvermindQuery.Type != "Deployment" { + t.Errorf("Expected query type to be Deployment, got %v", deployments[1].OvermindQuery.Type) } - // Since this resource is being deleted it doesn't have any config so we - // can't determine the scope from mappings - if queries[0].Scope != "*" { - t.Errorf("Expected query scope to be *, got %v", queries[0].Scope) + if deployments[1].OvermindQuery.Query != "api-server" { + t.Errorf("Expected query to be api-server, got %v", deployments[1].OvermindQuery.Query) } - if queries[1].Type != "Deployment" { - t.Errorf("Expected query type to be Deployment, got %v", queries[1].Type) + if deployments[1].OvermindQuery.Scope != "dogfood.default" { + t.Errorf("Expected query scope to be dogfood.default, got %v", deployments[1].OvermindQuery.Scope) } - if queries[1].Query != "api-server" { - t.Errorf("Expected query to be api-server, got %v", queries[1].Query) + iamPolicies, exists := mappedPlan.SupportedChanges["aws_iam_policy"] + + if !exists { + t.Errorf("Expected aws_iam_policy to be in supported changes") } - if queries[1].Scope != "dogfood.default" { - t.Errorf("Expected query scope to be dogfood.default, got %v", queries[1].Scope) + if len(iamPolicies) != 1 { + t.Errorf("Expected 1 iam policy, got %v", len(iamPolicies)) } - if queries[2].Type != "iam-policy" { - t.Errorf("Expected query type to be iam-policy, got %v", queries[2].Type) + if iamPolicies[0].OvermindQuery.Type != "iam-policy" { + t.Errorf("Expected query type to be iam-policy, got %v", iamPolicies[0].OvermindQuery.Type) } - if queries[2].Query != "arn:aws:iam::123456789012:policy/test-alb-ingress" { - t.Errorf("Expected query to be arn:aws:iam::123456789012:policy/test-alb-ingress, got %v", queries[2].Query) + if iamPolicies[0].OvermindQuery.Query != "arn:aws:iam::123456789012:policy/test-alb-ingress" { + t.Errorf("Expected query to be arn:aws:iam::123456789012:policy/test-alb-ingress, got %v", iamPolicies[0].OvermindQuery.Query) } - if queries[2].Scope != "*" { - t.Errorf("Expected query scope to be *, got %v", queries[2].Scope) + if iamPolicies[0].OvermindQuery.Scope != "*" { + t.Errorf("Expected query scope to be *, got %v", iamPolicies[0].OvermindQuery.Scope) } } diff --git a/go.mod b/go.mod index 213e4776..a71397eb 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ require ( github.com/bufbuild/connect-go v1.10.0 github.com/getsentry/sentry-go v0.23.0 github.com/google/uuid v1.3.1 - github.com/overmindtech/sdp-go v0.45.0 + github.com/mattn/go-isatty v0.0.19 + github.com/overmindtech/sdp-go v0.46.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 + github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 github.com/uptrace/opentelemetry-go-extra/otellogrus v0.2.2 github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b go.opentelemetry.io/contrib/detectors/aws/ec2 v1.17.0 diff --git a/go.sum b/go.sum index 58c42e28..4961bf82 100644 --- a/go.sum +++ b/go.sum @@ -180,8 +180,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -229,8 +227,8 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -241,8 +239,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4= -github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= +github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= github.com/nats-io/nats-server/v2 v2.9.20 h1:bt1dW6xsL1hWWwv7Hovm+EJt5L6iplyqlgEFkoEUk0k= github.com/nats-io/nats-server/v2 v2.9.20/go.mod h1:aTb/xtLCGKhfTFLxP591CMWfkdgBmcUUSkiSOe5A3gw= github.com/nats-io/nats.go v1.28.0 h1:Th4G6zdsz2d0OqXdfzKLClo6bOfoI/b1kInhRtFIy5c= @@ -251,10 +249,8 @@ github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA= github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/overmindtech/sdp-go v0.44.5 h1:NgjRwGmJWio8z40bOLo7OOd0Yeq9M9YIYpXrYOayyDs= -github.com/overmindtech/sdp-go v0.44.5/go.mod h1:9/yAXfMAAC0CQZ/pe3lxti/xL0cK27iOWpxYv11r5aw= -github.com/overmindtech/sdp-go v0.45.0 h1:2nvAaVoxlNZmWGqsctK1LSv2DJLdecmns5ulHkdQVPo= -github.com/overmindtech/sdp-go v0.45.0/go.mod h1:8bPomKpOpgy7GiTsh0rU4icSk3dKk+MaW+M8vuhEbSs= +github.com/overmindtech/sdp-go v0.46.0 h1:L2XXj5VVh1sTP5cBDp3qqU8hsPmfcnyQGSWgLTunS4k= +github.com/overmindtech/sdp-go v0.46.0/go.mod h1:Ety3j/it/DmnAE/eWBkPTMxXCc2vgTH9fXt7ePWpeYY= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -299,6 +295,8 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -493,6 +491,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/tracing/main.go b/tracing/main.go index e9ed9a80..376c2bb4 100644 --- a/tracing/main.go +++ b/tracing/main.go @@ -129,7 +129,6 @@ func InitTracer(opts ...otlptracehttp.Option) error { if err != nil { return fmt.Errorf("creating OTLP trace exporter: %w", err) } - log.Infof("otlptracehttp client configured itself: %v", client) tracerOpts := []sdktrace.TracerProviderOption{}