diff --git a/README.md b/README.md index 97a966b7..fc68c092 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Flags: Use "overmind [command] --help" for more information about a command. ``` +Set the environment variable `ACCESSIBLE` to `'true'` to enable screenreader mode. + ## Examples Upload a terraform plan to overmind for Blast Radius Analysis: diff --git a/cmd/bookmarks_create_bookmark.go b/cmd/bookmarks_create_bookmark.go index bb4e8c38..5c5719b1 100644 --- a/cmd/bookmarks_create_bookmark.go +++ b/cmd/bookmarks_create_bookmark.go @@ -87,7 +87,7 @@ func CreateBookmark(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:write"}) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 diff --git a/cmd/bookmarks_get_affected_bookmarks.go b/cmd/bookmarks_get_affected_bookmarks.go index e01f7d7d..ccced412 100644 --- a/cmd/bookmarks_get_affected_bookmarks.go +++ b/cmd/bookmarks_get_affected_bookmarks.go @@ -91,7 +91,7 @@ func GetAffectedBookmarks(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 diff --git a/cmd/bookmarks_get_bookmark.go b/cmd/bookmarks_get_bookmark.go index 802b3d0a..0107ba75 100644 --- a/cmd/bookmarks_get_bookmark.go +++ b/cmd/bookmarks_get_bookmark.go @@ -81,7 +81,7 @@ func GetBookmark(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 diff --git a/cmd/changes_end_change.go b/cmd/changes_end_change.go index 2a7a9331..a7999380 100644 --- a/cmd/changes_end_change.go +++ b/cmd/changes_end_change.go @@ -73,7 +73,7 @@ func EndChange(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:write"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -83,7 +83,7 @@ func EndChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, true) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, viper.GetString("ticket-link"), true) if err != nil { log.WithError(err).WithFields(lf).Error("failed to identify change") return 1 diff --git a/cmd/changes_get_change.go b/cmd/changes_get_change.go index fadab607..d1de4b1b 100644 --- a/cmd/changes_get_change.go +++ b/cmd/changes_get_change.go @@ -92,7 +92,7 @@ func GetChange(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -102,7 +102,7 @@ func GetChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus(sdp.ChangeStatus_value[viper.GetString("status")]), true) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus(sdp.ChangeStatus_value[viper.GetString("status")]), viper.GetString("ticket-link"), true) if err != nil { log.WithError(err).WithFields(lf).Error("failed to identify change") return 1 diff --git a/cmd/changes_list_changes.go b/cmd/changes_list_changes.go index ece1a95e..92f2fbb3 100644 --- a/cmd/changes_list_changes.go +++ b/cmd/changes_list_changes.go @@ -76,7 +76,7 @@ func ListChanges(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 diff --git a/cmd/changes_manual_change.go b/cmd/changes_manual_change.go index 51255062..676abe4d 100644 --- a/cmd/changes_manual_change.go +++ b/cmd/changes_manual_change.go @@ -77,7 +77,7 @@ func ManualChange(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:write"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -88,7 +88,7 @@ func ManualChange(ctx context.Context, ready chan bool) int { defer cancel() client := AuthenticatedChangesClient(ctx, oi) - changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, viper.GetString("ticket-link"),false) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to searching for existing changes") return 1 diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index daf04c2e..4cf06f29 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -73,7 +73,7 @@ func StartChange(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:write"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -83,7 +83,7 @@ func StartChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, true) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING,viper.GetString("ticket-link"), true) if err != nil { log.WithError(err).WithFields(lf).Error("failed to identify change") return 1 diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index f433a47d..9cafefed 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -17,7 +17,6 @@ import ( "connectrpc.com/connect" "github.com/getsentry/sentry-go" "github.com/google/uuid" - "github.com/overmindtech/cli/cmd/datamaps" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" @@ -25,7 +24,6 @@ import ( "github.com/spf13/viper" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/types/known/timestamppb" ) // submitPlanCmd represents the submit-plan command @@ -300,7 +298,7 @@ func isStateFile(bytes []byte) bool { return false } -func mappedItemDiffsFromPlan(ctx context.Context, fileName string, lf log.Fields) ([]*sdp.MappedItemDiff, error) { +func mappedItemDiffsFromPlanFile(ctx context.Context, fileName string, lf log.Fields) ([]*sdp.MappedItemDiff, error) { // read results from `terraform show -json ${tfplan file}` planJSON, err := os.ReadFile(fileName) if err != nil { @@ -308,211 +306,7 @@ func mappedItemDiffsFromPlan(ctx context.Context, fileName string, lf log.Fields return nil, err } - // Check that we haven't been passed a state file - if isStateFile(planJSON) { - return nil, fmt.Errorf("'%v' appears to be a state file, not a plan file", fileName) - } - - var plan Plan - err = json.Unmarshal(planJSON, &plan) - if err != nil { - return nil, fmt.Errorf("failed to parse '%v': %w", fileName, err) - } - - plannedChangeGroupsVar := plannedChangeGroups{ - supported: map[string][]*sdp.MappedItemDiff{}, - unsupported: map[string][]*sdp.MappedItemDiff{}, - } - - // for all managed resources: - for _, resourceChange := range plan.ResourceChanges { - if len(resourceChange.Change.Actions) == 0 || resourceChange.Change.Actions[0] == "no-op" || resourceChange.Mode == "data" { - // skip resources with no changes and data updates - continue - } - - itemDiff, err := itemDiffFromResourceChange(resourceChange) - if err != nil { - return nil, fmt.Errorf("failed to create item diff for resource change: %w", err) - } - - 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).Debug("Skipping unmapped resource") - plannedChangeGroupsVar.Add(resourceChange.Type, &sdp.MappedItemDiff{ - Item: itemDiff, - MappingQuery: nil, // unmapped item has no mapping query - }) - continue - } - - 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 - if plan.PriorState.Values != nil { - currentResource = plan.PriorState.Values.RootModule.DigResource(resourceChange.Address) - } - - // If we didn't find it, look in the planned values - if currentResource == nil { - currentResource = plan.PlannedValues.RootModule.DigResource(resourceChange.Address) - } - - if currentResource == nil { - log.WithContext(ctx). - WithFields(lf). - WithField("terraform-address", resourceChange.Address). - WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without values") - continue - } - - query, ok := currentResource.AttributeValues.Dig(mapData.QueryField) - if !ok { - log.WithContext(ctx). - WithFields(lf). - WithField("terraform-address", resourceChange.Address). - WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without query field") - continue - } - - // Create the map that variables will pull data from - dataMap := make(map[string]any) - - // Populate resource values - dataMap["values"] = currentResource.AttributeValues - - if overmindMappingsOutput, ok := plan.PlannedValues.Outputs["overmind_mappings"]; ok { - configResource := plan.Config.RootModule.DigResource(resourceChange.Address) - - if configResource == nil { - log.WithContext(ctx). - WithFields(lf). - WithField("terraform-address", resourceChange.Address). - 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) - - err = json.Unmarshal(overmindMappingsOutput.Value, &mappings) - - if err != nil { - log.WithContext(ctx). - WithFields(lf). - WithField("terraform-address", resourceChange.Address). - WithError(err). - Error("Failed to parse overmind_mappings output") - } else { - // We need to split out the module section of the name - // here. If the resource isn't in a module, the - // ProviderConfigKey will be something like - // "kubernetes", however if it's in a module it's be - // something like "module.something:kubernetes" - providerName := extractProviderNameFromConfigKey(configResource.ProviderConfigKey) - currentProviderMappings, ok := mappings[providerName] - - if ok { - log.WithContext(ctx). - WithFields(lf). - WithField("terraform-address", resourceChange.Address). - WithField("provider-config-key", configResource.ProviderConfigKey). - Debug("Found provider mappings") - - // We have mappings for this provider, so set them - // in the `provider_mapping` value - dataMap["provider_mapping"] = currentProviderMappings - } - } - } - } - - // Interpolate variables in the scope - scope, err := InterpolateScope(mapData.Scope, dataMap) - - if err != nil { - log.WithContext(ctx).WithError(err).Debugf("Could not find scope mapping variables %v, adding them will result in better results. Error: ", mapData.Scope) - scope = "*" - } - - u := uuid.New() - newQuery := &sdp.Query{ - Type: mapData.Type, - Method: mapData.Method, - Query: fmt.Sprintf("%v", query), - Scope: scope, - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(60 * time.Second)), - } - - // cleanup item metadata from mapping query - if itemDiff.GetBefore() != nil { - itemDiff.Before.Type = newQuery.GetType() - if newQuery.GetScope() != "*" { - itemDiff.Before.Scope = newQuery.GetScope() - } - } - - // cleanup item metadata from mapping query - if itemDiff.GetAfter() != nil { - itemDiff.After.Type = newQuery.GetType() - if newQuery.GetScope() != "*" { - itemDiff.After.Scope = newQuery.GetScope() - } - } - - plannedChangeGroupsVar.Add(resourceChange.Type, &sdp.MappedItemDiff{ - Item: itemDiff, - MappingQuery: newQuery, - }) - - log.WithContext(ctx).WithFields(log.Fields{ - "scope": newQuery.GetScope(), - "type": newQuery.GetType(), - "query": newQuery.GetQuery(), - "method": newQuery.GetMethod().String(), - }).Debug("Mapped resource to query") - } - } - - supported := "" - numSupported := plannedChangeGroupsVar.NumSupportedChanges() - if numSupported > 0 { - supported = Green.Color(fmt.Sprintf("%v supported", numSupported)) - } - - unsupported := "" - numUnsupported := plannedChangeGroupsVar.NumUnsupportedChanges() - if numUnsupported > 0 { - unsupported = Yellow.Color(fmt.Sprintf("%v unsupported", numUnsupported)) - } - - numTotalChanges := numSupported + numUnsupported - - switch numTotalChanges { - case 0: - log.WithContext(ctx).Infof("Plan (%v) contained no changing resources.", fileName) - case 1: - log.WithContext(ctx).Infof("Plan (%v) contained one changing resource: %v %v", fileName, supported, unsupported) - default: - log.WithContext(ctx).Infof("Plan (%v) contained %v changing resources: %v %v", fileName, numTotalChanges, supported, unsupported) - } - - // Log the types - for typ, plannedChanges := range plannedChangeGroupsVar.supported { - log.WithContext(ctx).Infof(Green.Color(" ✓ %v (%v)"), typ, len(plannedChanges)) - } - for typ, plannedChanges := range plannedChangeGroupsVar.unsupported { - log.WithContext(ctx).Infof(Yellow.Color(" ✗ %v (%v)"), typ, len(plannedChanges)) - } - - return plannedChangeGroupsVar.MappedItemDiffs(), nil + return mappedItemDiffsFromPlan(ctx, planJSON, fileName, lf) } // Returns the name of the provider from the config key. If the resource isn't @@ -589,7 +383,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:write"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -610,7 +404,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { for _, f := range files { lf["file"] = f - mappedItemDiffs, err := mappedItemDiffsFromPlan(ctx, f, lf) + mappedItemDiffs, err := mappedItemDiffsFromPlanFile(ctx, f, lf) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("Error parsing terraform plan") return 1 @@ -620,7 +414,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { delete(lf, "file") client := AuthenticatedChangesClient(ctx, oi) - changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, viper.GetString("ticket-link"), false) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed searching for existing changes") return 1 @@ -628,7 +422,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { title := changeTitle(viper.GetString("title")) tfPlanOutput := tryLoadText(ctx, viper.GetString("terraform-plan-output")) - codeChangessOutput := tryLoadText(ctx, viper.GetString("code-changes-diff")) + codeChangesOutput := tryLoadText(ctx, viper.GetString("code-changes-diff")) if changeUuid == uuid.Nil { log.WithContext(ctx).WithFields(lf).Debug("Creating a new change") @@ -641,7 +435,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { Owner: viper.GetString("owner"), // CcEmails: viper.GetString("cc-emails"), RawPlan: tfPlanOutput, - CodeChanges: codeChangessOutput, + CodeChanges: codeChangesOutput, }, }, }) @@ -673,7 +467,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { Owner: viper.GetString("owner"), // CcEmails: viper.GetString("cc-emails"), RawPlan: tfPlanOutput, - CodeChanges: codeChangessOutput, + CodeChanges: codeChangesOutput, }, }, }) diff --git a/cmd/changes_submit_plan_test.go b/cmd/changes_submit_plan_test.go index 1628baad..54ec5d7c 100644 --- a/cmd/changes_submit_plan_test.go +++ b/cmd/changes_submit_plan_test.go @@ -10,7 +10,7 @@ import ( ) func TestWithStateFile(t *testing.T) { - _, err := mappedItemDiffsFromPlan(context.Background(), "testdata/state.json", logrus.Fields{}) + _, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/state.json", logrus.Fields{}) if err == nil { t.Error("Expected error when running with state file, got none") @@ -18,7 +18,7 @@ func TestWithStateFile(t *testing.T) { } func TestMappedItemDiffsFromPlan(t *testing.T) { - mappedItemDiffs, err := mappedItemDiffsFromPlan(context.Background(), "testdata/plan.json", logrus.Fields{}) + mappedItemDiffs, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/plan.json", logrus.Fields{}) if err != nil { t.Error(err) diff --git a/cmd/charms.go b/cmd/charms.go new file mode 100644 index 00000000..047c10f2 --- /dev/null +++ b/cmd/charms.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/charmbracelet/glamour" +) + +var accessibleMode bool = os.Getenv("ACCESSIBLE") != "" + +// NewTermRenderer returns a glamour.TermRenderer with overmind defaults or panics +func NewTermRenderer() *glamour.TermRenderer { + r, err := glamour.NewTermRenderer( + // detect background color and pick either the default dark or light theme + glamour.WithAutoStyle(), + ) + if err != nil { + panic(fmt.Errorf("failed to initialize terminal renderer: %w", err)) + } + if r == nil { + panic("initialized terminal renderer is nil") + } + + return r +} diff --git a/cmd/invites_crud.go b/cmd/invites_crud.go index 3576293e..0f22768d 100644 --- a/cmd/invites_crud.go +++ b/cmd/invites_crud.go @@ -145,7 +145,7 @@ func InvitesRevoke(ctx context.Context) int { } // Authenticate - ctx, err = ensureToken(ctx, oi, []string{"account:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"account:write"}) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to ensure token") return 1 @@ -194,7 +194,7 @@ func InvitesCreate(ctx context.Context) int { } // Authenticate - ctx, err = ensureToken(ctx, oi, []string{"account:write"}) + ctx, _, err = ensureToken(ctx, oi, []string{"account:write"}) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to ensure token") return 1 @@ -237,7 +237,7 @@ func InvitesList(ctx context.Context) int { } // Authenticate - ctx, err = ensureToken(ctx, oi, []string{"account:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"account:read"}) if err != nil { log.WithError(err).Error("failed to ensure token") return 1 diff --git a/cmd/request.go b/cmd/request.go index ed4c8209..fd2f45c3 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -150,7 +150,7 @@ func Request(ctx context.Context, ready chan bool) int { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") return 1 } - ctx, err = ensureToken(ctx, oi, []string{"explore:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"explore:read"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 diff --git a/cmd/root.go b/cmd/root.go index e0d0b817..5af4e5dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,14 +15,19 @@ import ( "strings" "connectrpc.com/connect" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/huh/spinner" "github.com/google/uuid" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" + "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/uptrace/opentelemetry-go-extra/otellogrus" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "golang.org/x/oauth2" ) @@ -162,7 +167,7 @@ func extractClaims(token string) (*sdp.CustomClaims, error) { // its scopes, and an error if any. The scopes are returned even if they are // insufficient to allow cached tokens to be added to rather than constantly // replaced -func readLocalToken(homeDir string, expectedScopes []string) (string, []string, error) { +func readLocalToken(homeDir string, expectedScopes []string) (*oauth2.Token, []string, error) { // Read in the token JSON file path := filepath.Join(homeDir, ".overmind", "token.json") @@ -170,34 +175,34 @@ func readLocalToken(homeDir string, expectedScopes []string) (string, []string, // Check that the file exists if _, err := os.Stat(path); err != nil { - return "", nil, err + return nil, nil, err } // Read the file file, err := os.Open(path) if err != nil { - return "", nil, fmt.Errorf("error opening token file at %v: %w", path, err) + return nil, nil, fmt.Errorf("error opening token file at %v: %w", path, err) } // Decode the file err = json.NewDecoder(file).Decode(token) if err != nil { - return "", nil, fmt.Errorf("error decoding token file at %v: %w", path, err) + return nil, nil, fmt.Errorf("error decoding token file at %v: %w", path, err) } // Check to see if the token is still valid if !token.Valid() { - return "", nil, errors.New("token is no longer valid") + return nil, nil, errors.New("token is no longer valid") } claims, err := extractClaims(token.AccessToken) if err != nil { - return "", nil, fmt.Errorf("error extracting claims from token: %w", err) + return nil, nil, fmt.Errorf("error extracting claims from token: %w", err) } if claims.Scope == "" { - return "", nil, errors.New("token does not have any scopes") + return nil, nil, errors.New("token does not have any scopes") } currentScopes := strings.Split(claims.Scope, " ") @@ -205,12 +210,12 @@ func readLocalToken(homeDir string, expectedScopes []string) (string, []string, // Check that the token has the right scopes for _, scope := range expectedScopes { if !claims.HasScope(scope) { - return "", currentScopes, fmt.Errorf("token does not have required scope '%v'", scope) + return nil, currentScopes, fmt.Errorf("token does not have required scope '%v'", scope) } } log.Debugf("Using local token from %v", path) - return token.AccessToken, currentScopes, nil + return token, currentScopes, nil } // Check whether or not a token has all of the required scopes. Returns a @@ -233,10 +238,10 @@ func tokenHasAllScopes(token string, requiredScopes []string) (bool, error) { } // Gets a token using an API key -func getAPIKeyToken(ctx context.Context, oi OvermindInstance, apiKey string) (string, error) { +func getAPIKeyToken(ctx context.Context, oi OvermindInstance, apiKey string) (*oauth2.Token, error) { log.WithContext(ctx).Debug("using provided token for authentication") - var accessToken string + var token *oauth2.Token if strings.HasPrefix(apiKey, "ovm_api_") { // exchange api token for JWT @@ -247,25 +252,28 @@ func getAPIKeyToken(ctx context.Context, oi OvermindInstance, apiKey string) (st }, }) if err != nil { - return "", fmt.Errorf("error authenticating the API token: %w", err) + return nil, fmt.Errorf("error authenticating the API token: %w", err) } log.WithContext(ctx).Debug("successfully authenticated") - accessToken = resp.Msg.GetAccessToken() + token = &oauth2.Token{ + AccessToken: resp.Msg.GetAccessToken(), + TokenType: "Bearer", + } } else { - return "", errors.New("OVM_API_KEY does not match pattern 'ovm_api_*'") + return nil, errors.New("OVM_API_KEY does not match pattern 'ovm_api_*'") } - return accessToken, nil + return token, nil } // Gets a token from Oauth with the required scopes. This method will also cache // that token locally for use later, and will use the cached token if possible -func getOauthToken(ctx context.Context, requiredScopes []string) (string, error) { +func getOauthToken(ctx context.Context, requiredScopes []string) (*oauth2.Token, error) { var localScopes []string // Check for a locally saved token in ~/.overmind if home, err := os.UserHomeDir(); err == nil { - var localToken string + var localToken *oauth2.Token localToken, localScopes, err = readLocalToken(home, requiredScopes) @@ -286,11 +294,11 @@ func getOauthToken(ctx context.Context, requiredScopes []string) (string, error) parsed, err := url.Parse(appurl) if err != nil { log.WithContext(ctx).WithError(err).Error("Failed to parse --app") - return "", fmt.Errorf("error parsing --app: %w", err) + return nil, fmt.Errorf("error parsing --app: %w", err) } if !(parsed.Scheme == "wss" || parsed.Scheme == "https" || parsed.Hostname() == "localhost") { - return "", fmt.Errorf("target URL (%v) is insecure", parsed) + return nil, fmt.Errorf("target URL (%v) is insecure", parsed) } // If we need to get a new token, request the required scopes on top of // whatever ones the current local, valid token has so that we don't @@ -310,18 +318,54 @@ func getOauthToken(ctx context.Context, requiredScopes []string) (string, error) deviceCode, err := config.DeviceAuth(ctx, oauth2.SetAuthURLParam("audience", "https://api.overmind.tech")) if err != nil { - return "", fmt.Errorf("error getting device code: %w", err) + return nil, fmt.Errorf("error getting device code: %w", err) } - fmt.Printf("Go to %v and verify this code: %v\n", deviceCode.VerificationURIComplete, deviceCode.UserCode) + r := NewTermRenderer() + prompt := `# Authenticate with a browser + +Attempting to automatically open the SSO authorization page in your default browser. +If the browser does not open or you wish to use a different device to authorize this request, open the following URL: + + %v - token, err := config.DeviceAccessToken(ctx, deviceCode) +Then enter the code: + + %v +` + prompt = fmt.Sprintf(prompt, deviceCode.VerificationURI, deviceCode.UserCode) + out, err := glamour.Render(prompt, "dark") if err != nil { - fmt.Printf(": %v\n", err) - return "", fmt.Errorf("Error exchanging Device Code for for access token: %w", err) + panic(err) } + fmt.Print(out) - log.WithContext(ctx).Info("Authenticated successfully ✅") + err = browser.OpenURL(deviceCode.VerificationURIComplete) + if err != nil { + log.WithContext(ctx).WithError(err).Error("failed to execute local browser") + } + + var token *oauth2.Token + _ = spinner.New().Title("Waiting for confirmation...").Action(func() { + token, err = config.DeviceAccessToken(ctx, deviceCode) + if err != nil { + log.WithContext(ctx).WithError(err).Errorf("Error exchanging Device Code for for access token") + err = fmt.Errorf("Error exchanging Device Code for for access token: %w", err) + return + } + }).Run() + if err != nil { + return nil, err + } + + out, err = r.Render("✅ Authenticated successfully") + if err != nil { + panic(err) + } + fmt.Println(out) + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.Bool("ovm.cli.authenticated", true)) // Save the token locally if home, err := os.UserHomeDir(); err == nil { @@ -347,41 +391,40 @@ func getOauthToken(ctx context.Context, requiredScopes []string) (string, error) log.WithContext(ctx).Debugf("Saved token to %v", path) } - return token.AccessToken, nil + return token, nil } // ensureToken -func ensureToken(ctx context.Context, oi OvermindInstance, requiredScopes []string) (context.Context, error) { - var accessToken string +func ensureToken(ctx context.Context, oi OvermindInstance, requiredScopes []string) (context.Context, *oauth2.Token, error) { + var token *oauth2.Token var err error // get a token from the api key if present if apiKey := viper.GetString("api-key"); apiKey != "" { - accessToken, err = getAPIKeyToken(ctx, oi, apiKey) + token, err = getAPIKeyToken(ctx, oi, apiKey) } else { - accessToken, err = getOauthToken(ctx, requiredScopes) + token, err = getOauthToken(ctx, requiredScopes) } - if err != nil { - return ctx, fmt.Errorf("error getting token: %w", err) + return ctx, nil, fmt.Errorf("error getting token: %w", err) } // Check that we actually got the claims we asked for. If you don't have // permission auth0 will just not assign those scopes rather than fail - claims, err := extractClaims(accessToken) - + claims, err := extractClaims(token.AccessToken) if err != nil { - return ctx, fmt.Errorf("error extracting claims from token: %w", err) + return ctx, nil, fmt.Errorf("error extracting claims from token: %w", err) } ok, missing := HasScopesFlexible(claims, requiredScopes) - if !ok { - return ctx, fmt.Errorf("authenticated successfully, but you don't have the required permission: '%v'", missing) + return ctx, nil, fmt.Errorf("authenticated successfully, but you don't have the required permission: '%v'", missing) } // Add the token to the context - return context.WithValue(ctx, sdp.UserTokenContextKey{}, accessToken), nil + ctx = context.WithValue(ctx, sdp.UserTokenContextKey{}, token.AccessToken) + ctx = context.WithValue(ctx, sdp.AccountNameContextKey{}, claims.AccountName) + return ctx, token, nil } // Returns whether a set of claims has all of the required scopes. It also @@ -419,13 +462,12 @@ func HasScopesFlexible(claims *sdp.CustomClaims, requiredScopes []string) (bool, } // getChangeUuid returns the UUID of a change, as selected by --uuid or --change, or a state with the specified status and having --ticket-link -func getChangeUuid(ctx context.Context, oi OvermindInstance, expectedStatus sdp.ChangeStatus, errNotFound bool) (uuid.UUID, error) { +func getChangeUuid(ctx context.Context, oi OvermindInstance, expectedStatus sdp.ChangeStatus, ticketLink string, errNotFound bool) (uuid.UUID, error) { var changeUuid uuid.UUID var err error uuidString := viper.GetString("uuid") changeUrlString := viper.GetString("change") - ticketLink := viper.GetString("ticket-link") // If no arguments are specified then return an error if uuidString == "" && changeUrlString == "" && ticketLink == "" { diff --git a/cmd/snapshots_get_snapshot.go b/cmd/snapshots_get_snapshot.go index 36821d20..06aa2916 100644 --- a/cmd/snapshots_get_snapshot.go +++ b/cmd/snapshots_get_snapshot.go @@ -81,7 +81,7 @@ func GetSnapshot(ctx context.Context, ready chan bool) int { return 1 } - ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) + ctx, _, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go new file mode 100644 index 00000000..5df8b3d4 --- /dev/null +++ b/cmd/terraform_apply.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "connectrpc.com/connect" + "github.com/overmindtech/sdp-go" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/oauth2" +) + +// terraformApplyCmd represents the `terraform apply` command +var terraformApplyCmd = &cobra.Command{ + Use: "apply [overmind options...] -- [terraform options...]", + Short: "Runs `terraform apply` between two full system configuration snapshots for tracking. This will be automatically connected with the Change created by the `plan` command.", + 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 `terraform apply` flags") + } + }, + Run: CmdWrapper(TerraformApply, []string{"changes:write", "request:receive"}), +} + +func TerraformApply(ctx context.Context, args []string, oi OvermindInstance, token *oauth2.Token) error { + cancel, err := InitializeSources(ctx, oi, token) + defer cancel() + if err != nil { + return err + } + + ticketLink := viper.GetString("ticket-link") + if ticketLink == "" { + ticketLink, err = getTicketLinkFromPlan() + if err != nil { + return err + } + } + + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, ticketLink, true) + if err != nil { + return fmt.Errorf("failed to identify change: %w", err) + } + + client := AuthenticatedChangesClient(ctx, oi) + startStream, err := client.StartChange(ctx, &connect.Request[sdp.StartChangeRequest]{ + Msg: &sdp.StartChangeRequest{ + ChangeUUID: changeUuid[:], + }, + }) + if err != nil { + return fmt.Errorf("failed to start change: %w", err) + } + for startStream.Receive() { + msg := startStream.Msg() + log.WithFields(log.Fields{ + "state": msg.GetState(), + "items": msg.GetNumItems(), + "edges": msg.GetNumEdges(), + }).Info("progress") + } + if startStream.Err() != nil { + return fmt.Errorf("failed to process start change: %w", startStream.Err()) + } + + args = append([]string{"apply"}, args...) + // plan file needs to go last + args = append(args, "overmind.plan") + + prompt := ` +* AWS Source: running +* stdlib Source: running + +# Applying Changes + +Running ` + "`" + `terraform %v` + "`" + ` +` + + r := NewTermRenderer() + out, err := r.Render(fmt.Sprintf(prompt, strings.Join(args, " "))) + if err != nil { + panic(err) + } + fmt.Print(out) + + tfApplyCmd := exec.CommandContext(ctx, "terraform", args...) + tfApplyCmd.Stderr = os.Stderr + tfApplyCmd.Stdout = os.Stdout + tfApplyCmd.Stdin = os.Stdin + + err = tfApplyCmd.Run() + if err != nil { + return fmt.Errorf("failed to run terraform apply: %w", err) + } + + endStream, err := client.EndChange(ctx, &connect.Request[sdp.EndChangeRequest]{ + Msg: &sdp.EndChangeRequest{ + ChangeUUID: changeUuid[:], + }, + }) + if err != nil { + return fmt.Errorf("failed to end change: %w", err) + } + for endStream.Receive() { + msg := endStream.Msg() + log.WithFields(log.Fields{ + "state": msg.GetState(), + "items": msg.GetNumItems(), + "edges": msg.GetNumEdges(), + }).Info("progress") + } + if endStream.Err() != nil { + return fmt.Errorf("failed to process end change: %w", endStream.Err()) + } + + return nil +} + +func init() { + terraformCmd.AddCommand(terraformApplyCmd) + + addAPIFlags(terraformApplyCmd) + addChangeUuidFlags(terraformApplyCmd) +} diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go new file mode 100644 index 00000000..2607cffd --- /dev/null +++ b/cmd/terraform_plan.go @@ -0,0 +1,623 @@ +package cmd + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" + + "connectrpc.com/connect" + "github.com/charmbracelet/huh" + "github.com/google/uuid" + awssource "github.com/overmindtech/aws-source/cmd" + "github.com/overmindtech/cli/cmd/datamaps" + "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/sdp-go" + "github.com/overmindtech/sdp-go/auth" + stdlibsource "github.com/overmindtech/stdlib-source/cmd" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "golang.org/x/oauth2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// terraformPlanCmd represents the `terraform plan` command +var terraformPlanCmd = &cobra.Command{ + Use: "plan [overmind options...] -- [terraform options...]", + Short: "Runs `terraform plan` and sends the results to Overmind to calculate a blast radius and risks.", + 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 `terraform plan` flags") + } + }, + Run: CmdWrapper(TerraformPlan, []string{"changes:write", "request:receive"}), +} + +type OvermindCommandHandler func(ctx context.Context, args []string, oi OvermindInstance, token *oauth2.Token) error + +func CmdWrapper(handler OvermindCommandHandler, requiredScopes []string) func(cmd *cobra.Command, args []string) { + return 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(): + } + }() + + ctx, span := tracing.Tracer().Start(ctx, fmt.Sprintf("CLI %v", cmd.CommandPath()), trace.WithAttributes( + attribute.String("ovm.config", fmt.Sprintf("%v", viper.AllSettings())), + )) + defer span.End() + + // wrap the rest of the function in a closure to allow for cleaner error handling and deferring. + err := func() error { + timeout, err := time.ParseDuration(viper.GetString("timeout")) + if err != nil { + return fmt.Errorf("invalid --timeout value '%v', error: %w", viper.GetString("timeout"), err) + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + return fmt.Errorf("failed to get instance data from app: %w", err) + } + + ctx, token, err := ensureToken(ctx, oi, requiredScopes) + if err != nil { + return fmt.Errorf("failed to authenticate: %w", err) + } + + // apply a timeout to the main body of processing + _, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return handler(ctx, args, oi, token) + }() + if err != nil { + log.WithContext(ctx).WithError(err).Error("Error running command") + // don't forget that os.Exit() does not wait for telemetry to be flushed + span.End() + tracing.ShutdownTracer() + os.Exit(1) + } + } +} + +func InitializeSources(ctx context.Context, oi OvermindInstance, token *oauth2.Token) (func(), error) { + hostname, err := os.Hostname() + if err != nil { + hostname = "localhost" + } + + natsNamePrefix := "overmind-cli" + + openapiUrl := *oi.ApiUrl + openapiUrl.Path = "/api" + tokenClient := auth.NewOAuthTokenClientWithContext( + ctx, + openapiUrl.String(), + "", + oauth2.StaticTokenSource(token), + ) + + natsOptions := auth.NATSOptions{ + NumRetries: 3, + RetryDelay: 1 * time.Second, + Servers: []string{oi.NatsUrl.String()}, + ConnectionName: fmt.Sprintf("%v.%v", natsNamePrefix, hostname), + ConnectionTimeout: (10 * time.Second), // TODO: Make configurable + MaxReconnects: -1, + ReconnectWait: 1 * time.Second, + ReconnectJitter: 1 * time.Second, + TokenClient: tokenClient, + } + + // TODO: store this in the api-server and skip questioning the user after the first time + aws_config := "aborted" + options := []huh.Option[string]{} + aws_profile := os.Getenv("AWS_PROFILE") + if aws_profile != "" { + options = append(options, + huh.NewOption(fmt.Sprintf("Use $AWS_PROFILE (currently: '%v')", aws_profile), "aws_profile"), + huh.NewOption("Use a different profile", "profile_input"), + ) + } else { + options = append(options, + huh.NewOption("Use the default settings", "defaults"), + huh.NewOption("Use an AWS SSO profile", "profile_input"), + ) + } + // TODO: what URL needs to get opened here? + // TODO: how to wait for a source to be configured? + // options = append(options, + // huh.NewOption("Run managed source (opens browser)", "managed"), + // ) + aws_config_select := huh.NewSelect[string](). + Title("Choose how to access your AWS account (read-only):"). + Options(options...). + Value(&aws_config). + WithAccessible(accessibleMode) + err = aws_config_select.Run() + // annoyingly, huh doesn't leave the form on screen - except in + // accessible mode, so this prints it again so the scrollback looks + // sensible + if !accessibleMode { + fmt.Println(aws_config_select.View()) + } + if err != nil { + return func() {}, err + } + + awsAuthConfig := awssource.AwsAuthConfig{ + // TODO: query regions + Regions: []string{"eu-west-1"}, + } + + switch aws_config { + case "profile_input": + aws_profile_input := huh.NewInput(). + Title("Input the name of the AWS profile to use:"). + Value(&aws_profile). + WithAccessible(accessibleMode) + err = aws_profile_input.Run() + // annoyingly, huh doesn't leave the form on screen - except in + // accessible mode, so this prints it again so the scrollback looks + // sensible + if !accessibleMode { + fmt.Println(aws_profile_input.View()) + } + if err != nil { + return func() {}, err + } + // reset the environment to the requested value + awsAuthConfig.Strategy = "sso-profile" + awsAuthConfig.Profile = aws_profile + case "aws_profile": + // can continue with the existing config + awsAuthConfig.Strategy = "sso-profile" + awsAuthConfig.Profile = aws_profile + case "defaults": + // just continue + awsAuthConfig.Strategy = "defaults" + case "managed": + // TODO: not implemented yet + } + + awsEngine, err := awssource.InitializeAwsSourceEngine(natsOptions, awsAuthConfig, 2_000) + if err != nil { + return func() {}, fmt.Errorf("failed to initialize AWS source engine: %w", err) + } + + // todo: pass in context with timeout to abort timely and allow Ctrl-C to work + err = awsEngine.Start() + if err != nil { + return func() {}, fmt.Errorf("failed to start AWS source engine: %w", err) + } + + stdlibEngine, err := stdlibsource.InitializeStdlibSourceEngine(natsOptions, 2_000, true) + if err != nil { + return func() { + _ = awsEngine.Stop() + }, fmt.Errorf("failed to initialize stdlib source engine: %w", err) + } + + // todo: pass in context with timeout to abort timely and allow Ctrl-C to work + err = stdlibEngine.Start() + if err != nil { + return func() { + _ = awsEngine.Stop() + }, fmt.Errorf("failed to start stdlib source engine: %w", err) + } + + return func() { + _ = awsEngine.Stop() + _ = stdlibEngine.Stop() + }, nil +} + +func TerraformPlan(ctx context.Context, args []string, oi OvermindInstance, token *oauth2.Token) error { + span := trace.SpanFromContext(ctx) + + cancel, err := InitializeSources(ctx, oi, token) + defer cancel() + if err != nil { + return err + } + + args = append([]string{"plan"}, args...) + // -out needs to go last to override whatever the user specified on the command line + args = append(args, "-out", "overmind.plan") + + prompt := ` +* AWS Source: running +* stdlib Source: running + +# Planning Changes + +Running ` + "`" + `terraform %v` + "`" + ` +` + + r := NewTermRenderer() + out, err := r.Render(fmt.Sprintf(prompt, strings.Join(args, " "))) + if err != nil { + panic(err) + } + fmt.Print(out) + + tfPlanCmd := exec.CommandContext(ctx, "terraform", args...) + tfPlanCmd.Stderr = os.Stderr + tfPlanCmd.Stdout = os.Stdout + tfPlanCmd.Stdin = os.Stdin + + err = tfPlanCmd.Run() + if err != nil { + return fmt.Errorf("failed to run terraform plan: %w", err) + } + + tfPlanJsonCmd := exec.CommandContext(ctx, "terraform", "show", "-json", "overmind.plan") + tfPlanJsonCmd.Stderr = os.Stderr + + planJson, err := tfPlanJsonCmd.Output() + if err != nil { + return fmt.Errorf("failed to convert terraform plan to JSON: %w", err) + } + + plannedChanges, err := mappedItemDiffsFromPlan(ctx, planJson, "overmind.plan", log.Fields{}) + if err != nil { + return fmt.Errorf("failed to parse terraform plan: %w", err) + } + + ticketLink := viper.GetString("ticket-link") + if ticketLink == "" { + ticketLink, err = getTicketLinkFromPlan() + if err != nil { + return err + } + } + + client := AuthenticatedChangesClient(ctx, oi) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, ticketLink, false) + if err != nil { + return fmt.Errorf("failed searching for existing changes: %w", err) + } + + title := changeTitle(viper.GetString("title")) + tfPlanOutput := tryLoadText(ctx, viper.GetString("terraform-plan-output")) + codeChangesOutput := tryLoadText(ctx, viper.GetString("code-changes-diff")) + + if changeUuid == uuid.Nil { + log.Debug("Creating a new change") + createResponse, err := client.CreateChange(ctx, &connect.Request[sdp.CreateChangeRequest]{ + Msg: &sdp.CreateChangeRequest{ + Properties: &sdp.ChangeProperties{ + Title: title, + Description: viper.GetString("description"), + TicketLink: ticketLink, + Owner: viper.GetString("owner"), + // CcEmails: viper.GetString("cc-emails"), + RawPlan: tfPlanOutput, + CodeChanges: codeChangesOutput, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to create change: %w", err) + } + + maybeChangeUuid := createResponse.Msg.GetChange().GetMetadata().GetUUIDParsed() + if maybeChangeUuid == nil { + return fmt.Errorf("failed to read change id: %w", err) + } + + changeUuid = *maybeChangeUuid + span.SetAttributes( + attribute.String("ovm.change.uuid", changeUuid.String()), + attribute.Bool("ovm.change.new", true), + ) + } else { + log.WithField("change", changeUuid).Debug("Updating an existing change") + span.SetAttributes( + attribute.String("ovm.change.uuid", changeUuid.String()), + attribute.Bool("ovm.change.new", false), + ) + + _, err := client.UpdateChange(ctx, &connect.Request[sdp.UpdateChangeRequest]{ + Msg: &sdp.UpdateChangeRequest{ + UUID: changeUuid[:], + Properties: &sdp.ChangeProperties{ + Title: title, + Description: viper.GetString("description"), + TicketLink: ticketLink, + Owner: viper.GetString("owner"), + // CcEmails: viper.GetString("cc-emails"), + RawPlan: tfPlanOutput, + CodeChanges: codeChangesOutput, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to update change: %w", err) + } + } + + log.WithField("change", changeUuid).Debug("Uploading planned changes") + + resultStream, err := client.UpdatePlannedChanges(ctx, &connect.Request[sdp.UpdatePlannedChangesRequest]{ + Msg: &sdp.UpdatePlannedChangesRequest{ + ChangeUUID: changeUuid[:], + ChangingItems: plannedChanges, + }, + }) + if err != nil { + return fmt.Errorf("failed to update planned changes: %w", err) + } + + last_log := time.Now() + first_log := true + for resultStream.Receive() { + msg := resultStream.Msg() + + // log the first message and at most every 250ms during discovery + // to avoid spanning the cli output + time_since_last_log := time.Since(last_log) + if first_log || msg.GetState() != sdp.CalculateBlastRadiusResponse_STATE_DISCOVERING || time_since_last_log > 250*time.Millisecond { + log.WithField("msg", msg).Info("Status update") + last_log = time.Now() + first_log = false + } + } + if resultStream.Err() != nil { + return fmt.Errorf("error streaming results: %w", resultStream.Err()) + } + + changeUrl := *oi.FrontendUrl + changeUrl.Path = fmt.Sprintf("%v/changes/%v/blast-radius", changeUrl.Path, changeUuid) + log.WithField("change-url", changeUrl.String()).Info("Change ready") + fmt.Println(changeUrl.String()) + return nil +} + +// getTicketLinkFromPlan reads the plan file to create a unique hash to identify this change +func getTicketLinkFromPlan() (string, error) { + plan, err := os.ReadFile("overmind.plan") + if err != nil { + return "", fmt.Errorf("failed to read overmind.plan file: %w", err) + } + h := sha256.New() + h.Write(plan) + return fmt.Sprintf("tfplan://{SHA256}%x", h.Sum(nil)), nil +} + +func mappedItemDiffsFromPlan(ctx context.Context, planJson []byte, fileName string, lf log.Fields) ([]*sdp.MappedItemDiff, error) { + // Check that we haven't been passed a state file + if isStateFile(planJson) { + return nil, fmt.Errorf("'%v' appears to be a state file, not a plan file", fileName) + } + + var plan Plan + err := json.Unmarshal(planJson, &plan) + if err != nil { + return nil, fmt.Errorf("failed to parse '%v': %w", fileName, err) + } + + plannedChangeGroupsVar := plannedChangeGroups{ + supported: map[string][]*sdp.MappedItemDiff{}, + unsupported: map[string][]*sdp.MappedItemDiff{}, + } + + // for all managed resources: + for _, resourceChange := range plan.ResourceChanges { + if len(resourceChange.Change.Actions) == 0 || resourceChange.Change.Actions[0] == "no-op" || resourceChange.Mode == "data" { + // skip resources with no changes and data updates + continue + } + + itemDiff, err := itemDiffFromResourceChange(resourceChange) + if err != nil { + return nil, fmt.Errorf("failed to create item diff for resource change: %w", err) + } + + 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).Debug("Skipping unmapped resource") + plannedChangeGroupsVar.Add(resourceChange.Type, &sdp.MappedItemDiff{ + Item: itemDiff, + MappingQuery: nil, // unmapped item has no mapping query + }) + continue + } + + 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 + if plan.PriorState.Values != nil { + currentResource = plan.PriorState.Values.RootModule.DigResource(resourceChange.Address) + } + + // If we didn't find it, look in the planned values + if currentResource == nil { + currentResource = plan.PlannedValues.RootModule.DigResource(resourceChange.Address) + } + + if currentResource == nil { + log.WithContext(ctx). + WithFields(lf). + WithField("terraform-address", resourceChange.Address). + WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without values") + continue + } + + query, ok := currentResource.AttributeValues.Dig(mapData.QueryField) + if !ok { + log.WithContext(ctx). + WithFields(lf). + WithField("terraform-address", resourceChange.Address). + WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without query field") + continue + } + + // Create the map that variables will pull data from + dataMap := make(map[string]any) + + // Populate resource values + dataMap["values"] = currentResource.AttributeValues + + if overmindMappingsOutput, ok := plan.PlannedValues.Outputs["overmind_mappings"]; ok { + configResource := plan.Config.RootModule.DigResource(resourceChange.Address) + + if configResource == nil { + log.WithContext(ctx). + WithFields(lf). + WithField("terraform-address", resourceChange.Address). + 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) + + err = json.Unmarshal(overmindMappingsOutput.Value, &mappings) + + if err != nil { + log.WithContext(ctx). + WithFields(lf). + WithField("terraform-address", resourceChange.Address). + WithError(err). + Error("Failed to parse overmind_mappings output") + } else { + // We need to split out the module section of the name + // here. If the resource isn't in a module, the + // ProviderConfigKey will be something like + // "kubernetes", however if it's in a module it's be + // something like "module.something:kubernetes" + providerName := extractProviderNameFromConfigKey(configResource.ProviderConfigKey) + currentProviderMappings, ok := mappings[providerName] + + if ok { + log.WithContext(ctx). + WithFields(lf). + WithField("terraform-address", resourceChange.Address). + WithField("provider-config-key", configResource.ProviderConfigKey). + Debug("Found provider mappings") + + // We have mappings for this provider, so set them + // in the `provider_mapping` value + dataMap["provider_mapping"] = currentProviderMappings + } + } + } + } + + // Interpolate variables in the scope + scope, err := InterpolateScope(mapData.Scope, dataMap) + + if err != nil { + log.WithContext(ctx).WithError(err).Debugf("Could not find scope mapping variables %v, adding them will result in better results. Error: ", mapData.Scope) + scope = "*" + } + + u := uuid.New() + newQuery := &sdp.Query{ + Type: mapData.Type, + Method: mapData.Method, + Query: fmt.Sprintf("%v", query), + Scope: scope, + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(60 * time.Second)), + } + + // cleanup item metadata from mapping query + if itemDiff.GetBefore() != nil { + itemDiff.Before.Type = newQuery.GetType() + if newQuery.GetScope() != "*" { + itemDiff.Before.Scope = newQuery.GetScope() + } + } + + // cleanup item metadata from mapping query + if itemDiff.GetAfter() != nil { + itemDiff.After.Type = newQuery.GetType() + if newQuery.GetScope() != "*" { + itemDiff.After.Scope = newQuery.GetScope() + } + } + + plannedChangeGroupsVar.Add(resourceChange.Type, &sdp.MappedItemDiff{ + Item: itemDiff, + MappingQuery: newQuery, + }) + + log.WithContext(ctx).WithFields(log.Fields{ + "scope": newQuery.GetScope(), + "type": newQuery.GetType(), + "query": newQuery.GetQuery(), + "method": newQuery.GetMethod().String(), + }).Debug("Mapped resource to query") + } + } + + supported := "" + numSupported := plannedChangeGroupsVar.NumSupportedChanges() + if numSupported > 0 { + supported = Green.Color(fmt.Sprintf("%v supported", numSupported)) + } + + unsupported := "" + numUnsupported := plannedChangeGroupsVar.NumUnsupportedChanges() + if numUnsupported > 0 { + unsupported = Yellow.Color(fmt.Sprintf("%v unsupported", numUnsupported)) + } + + numTotalChanges := numSupported + numUnsupported + + switch numTotalChanges { + case 0: + log.WithContext(ctx).Infof("Plan (%v) contained no changing resources.", fileName) + case 1: + log.WithContext(ctx).Infof("Plan (%v) contained one changing resource: %v %v", fileName, supported, unsupported) + default: + log.WithContext(ctx).Infof("Plan (%v) contained %v changing resources: %v %v", fileName, numTotalChanges, supported, unsupported) + } + + // Log the types + for typ, plannedChanges := range plannedChangeGroupsVar.supported { + log.WithContext(ctx).Infof(Green.Color(" ✓ %v (%v)"), typ, len(plannedChanges)) + } + for typ, plannedChanges := range plannedChangeGroupsVar.unsupported { + log.WithContext(ctx).Infof(Yellow.Color(" ✗ %v (%v)"), typ, len(plannedChanges)) + } + + return plannedChangeGroupsVar.MappedItemDiffs(), nil +} + +func init() { + terraformCmd.AddCommand(terraformPlanCmd) + + addAPIFlags(terraformPlanCmd) + addChangeUuidFlags(terraformPlanCmd) +} diff --git a/go.mod b/go.mod index bf89959d..44c5664d 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,18 @@ go 1.22.0 require ( connectrpc.com/connect v1.15.0 + github.com/charmbracelet/glamour v0.6.0 + github.com/charmbracelet/huh v0.3.0 + github.com/charmbracelet/huh/spinner v0.0.0-20240209193029-45947515c4cf github.com/getsentry/sentry-go v0.27.0 github.com/google/uuid v1.6.0 github.com/hexops/gotextdiff v1.0.3 github.com/jedib0t/go-pretty/v6 v6.5.4 github.com/mattn/go-isatty v0.0.20 + github.com/overmindtech/aws-source v0.0.0-20240228174133-d45f6f687ef8 github.com/overmindtech/sdp-go v0.66.2 + github.com/overmindtech/stdlib-source v0.0.0-20240228111344-75ee76fc53e7 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 @@ -31,29 +37,98 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/MrAlias/otel-schema-utils v0.2.1-alpha // indirect + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/alecthomas/kingpin/v2 v2.3.2 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/auth0/go-jwt-middleware/v2 v2.2.1 // indirect github.com/aws/aws-sdk-go v1.50.15 // indirect + github.com/aws/aws-sdk-go-v2 v1.25.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 // indirect + github.com/aws/aws-sdk-go-v2/service/autoscaling v1.40.0 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.35.0 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/directconnect v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.30.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.149.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/service/efs v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.40.0 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.0 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect + github.com/aws/aws-sdk-go-v2/service/lambda v1.53.0 // indirect + github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.38.0 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.72.0 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.40.0 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 // indirect + github.com/aws/smithy-go v1.20.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.17.5 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/microcosm-cc/bluemonday v1.0.23 // indirect + github.com/miekg/dns v1.1.58 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/nats-io/jwt/v2 v2.5.5 // indirect github.com/nats-io/nats.go v1.32.0 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/openrdap/rdap v0.9.2-0.20240219192926-2210b535d40b // indirect + github.com/overmindtech/api-client v0.14.0 // indirect + github.com/overmindtech/discovery v0.26.3 // indirect + github.com/overmindtech/sdpcache v1.6.3 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/rivo/uniseg v0.4.6 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -62,16 +137,23 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.2.3 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect + github.com/yuin/goldmark v1.5.2 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/schema v0.0.7 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect diff --git a/go.sum b/go.sum index 966f8e62..5e535e97 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,123 @@ connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo= connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/MrAlias/otel-schema-utils v0.2.1-alpha h1:dSeMM04tO+EY1JLof0bL8rIDkaMV3yBiUFdcSeIfbqI= +github.com/MrAlias/otel-schema-utils v0.2.1-alpha/go.mod h1:i5gQR7dVLC4XxJuPITWxpWnjGRICZY50OMXXtrQTDeQ= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= +github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/auth0/go-jwt-middleware/v2 v2.2.1 h1:pqxEIwlCztD0T9ZygGfOrw4NK/F9iotnCnPJVADKbkE= github.com/auth0/go-jwt-middleware/v2 v2.2.1/go.mod h1:CSi0tuu0QrALbWdiQZwqFL8SbBhj4e2MJzkvNfjY0Us= github.com/aws/aws-sdk-go v1.50.15 h1:wEMnPfEQQFaoIJwuO18zq/vtG4Ft7NxQ3r9xlEi/8zg= github.com/aws/aws-sdk-go v1.50.15/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.25.1 h1:P7hU6A5qEdmajGwvae/zDkOq+ULLC9tQBTwqqiwFGpI= +github.com/aws/aws-sdk-go-v2 v1.25.1/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo= +github.com/aws/aws-sdk-go-v2/config v1.27.3 h1:0PRdb/q5a77HVYj+2rvPiCObfMfl/pWhwa5cs3cnl3c= +github.com/aws/aws-sdk-go-v2/config v1.27.3/go.mod h1:WeRAr9ENap9NAegbfNsLqGQd8ERz5ypdIUx4j0/ZgKI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.3 h1:dDM5wrgwOL5gTZ0Gv/bvewPldjBcJywoaO5ClERrOGE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.3/go.mod h1:G96Nuaw9qJS+s3OnK8RW8VEKEOjXi8H5Jk4lC/ZyZbw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 h1:lk1ZZFbdb24qpOwVC1AwYNrswUjAxeyey6kFBVANudQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1/go.mod h1:/xJ6x1NehNGCX4tvGzzj2bq5TBOT/Yxq+qbL9Jpx2Vk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 h1:evvi7FbTAoFxdP/mixmP7LIYzQWAmzBcwNB/es9XPNc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1/go.mod h1:rH61DT6FDdikhPghymripNUCsf+uVF4Cnk4c4DBKH64= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 h1:RAnaIrbxPtlXNVI/OIlh1sidTQ3e1qM6LRjs7N0bE0I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1/go.mod h1:nbgAGkH5lk0RZRMh6A4K/oG6Xj11eC/1CyDow+DUAFI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 h1:rtYJd3w6IWCTVS8vmMaiXjW198noh2PBm5CiXyJea9o= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1/go.mod h1:zvXu+CTlib30LUy4LTNFc6HTZ/K6zCae5YIHTdX9wIo= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.40.0 h1:3J4yd6QKkky33TsVdg8E+CtrGXuJykkmtexVVddCbnE= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.40.0/go.mod h1:PuMdHiHJO2opf7qS8EGVCX2wKtC0zNB6TGxP2PQEjNk= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.35.0 h1:uOnKCqN08dGLVgo1s9HkClk+x8EjGO2F/k8vo8F94H4= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.35.0/go.mod h1:AP6K53k+LOhRKXUNSpGE8NKqYuo0firmy5VxvSUA5NI= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.0 h1:UiL5Ye6lCCOItI511dNFJh5zP2X40fPOns6LouaV+7Y= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.0/go.mod h1:72ZIKWxrPIXI+2HbO50zVNlf5EWFJfcxCUm+CNw3Vu0= +github.com/aws/aws-sdk-go-v2/service/directconnect v1.24.0 h1:nWNRK0WIP5kYQgcFk36h9fKZFVwUXsS1g2qf1Zy6vYE= +github.com/aws/aws-sdk-go-v2/service/directconnect v1.24.0/go.mod h1:2krWl7mX7aGGgMSFkHdJVcn8tt3WZ4FLtWG0rbJRmW0= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.30.0 h1:rZ2DPklkMHMFGUe1GbtfBJjPa+1M6JUemDntzgQaA7Y= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.30.0/go.mod h1:H6ktm/kjq2KtbGwnVFMAyOkOwcFfoD0P+SpneVqaa5o= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.149.0 h1:uMw4dz7s741WxewdyxOV7n8Rgajf6Azy+tx0VoJRm6k= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.149.0/go.mod h1:7MUTgVVnC1GAxx4SNQqzQalrm1n4v1HYa/R/LEB3CKo= +github.com/aws/aws-sdk-go-v2/service/ecs v1.41.0 h1:egUlYUhcL4zlooph2/32iG9Dpn02No8jhDyuZnFUGa4= +github.com/aws/aws-sdk-go-v2/service/ecs v1.41.0/go.mod h1:n+f+SkqQku/MS0qGvdM1NZR19waJGib/giugn42uHt0= +github.com/aws/aws-sdk-go-v2/service/efs v1.28.0 h1:rYGtiiPAAajBlOrgOftYJBcs586n9P6ceRrj04oM7tA= +github.com/aws/aws-sdk-go-v2/service/efs v1.28.0/go.mod h1:IfS8DFczmdfkSew41MZkycj9b84ZVRf7Bhx4SDebVIs= +github.com/aws/aws-sdk-go-v2/service/eks v1.40.0 h1:5juNqJl4h8dtlD13TdRobxKdSTVkz2XYk8gmJv3tUGs= +github.com/aws/aws-sdk-go-v2/service/eks v1.40.0/go.mod h1:gzXWmY937TLpT2GJbYBfgb664o/GlwkNNWQ6RGPFyeQ= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.24.0 h1:55OLwyc6YQL9D61iK2UT3uvO2zAHYW1L6PyMOuavq/g= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.24.0/go.mod h1:3PJ6pzBYxT+MV1IMB3ZkqS7zoF87hwmaL9UPH+83B98= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.0 h1:oZw6t5KJaIoz9KrYLsxB0OifIYV87eX1pdZB16xznO8= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.0/go.mod h1:Dj2PCO3btmFT93DNSrnCwokT3BqAUSXs0JpMUx5MO/E= +github.com/aws/aws-sdk-go-v2/service/iam v1.31.0 h1:fm/1QEydjes8ge1ab58/Ffdv/rI9uwidEYNHTHs5Qpc= +github.com/aws/aws-sdk-go-v2/service/iam v1.31.0/go.mod h1:ez+2dd+lsGYOg/rvCFauUnhdCtyOS+ARj2deYCGETkY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 h1:5Wxh862HkXL9CbQ83BIkWKLIgQapGeuh5zG2G9OZtQk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1/go.mod h1:V7GLA01pNUxMCYSQsibdVrqUrNIYIT/9lCOyR8ExNvQ= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.1 h1:QEot4yoGf6KGY2hAJe7IIP5x51pyRv4cs/x/aKcXMck= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.1/go.mod h1:FVivjmCWEidMuFguqtnXZGoJK/MN+EtoCSEZMEcpGhc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 h1:cVP8mng1RjDyI3JN/AXFCn5FHNlsBaBH0/MBtG1bg0o= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1/go.mod h1:C8sQjoyAsdfjC7hpy4+S6B92hnFzx0d0UAyHicaOTIE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 h1:OYmmIcyw19f7x0qLBLQ3XsrCZSSyLhxd9GXng5evsN4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1/go.mod h1:s5rqdn74Vdg10k61Pwf4ZHEApOSD6CKRe6qpeHDq32I= +github.com/aws/aws-sdk-go-v2/service/lambda v1.53.0 h1:EPsyL8kIjSQnShTQXZIWYTPVWSgWpG7ZtQRoJuqqf6Y= +github.com/aws/aws-sdk-go-v2/service/lambda v1.53.0/go.mod h1:4eq2dibHpzftx3fHVaHLjw7ab/DlUM2GddPb0Vxg7+8= +github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.38.0 h1:6etY7WF0D3R6CIBW2w9InXLoqcREtueMX87ZcsOMHwo= +github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.38.0/go.mod h1:WCxKTMr5kfKE71JtCB5lIhz9G56jxhMbTB1d+wTdlV4= +github.com/aws/aws-sdk-go-v2/service/rds v1.72.0 h1:ZEJp9Gh+g3rX/K7FEpoXaFoY+ga7Fxp92w2bHNGgihY= +github.com/aws/aws-sdk-go-v2/service/rds v1.72.0/go.mod h1:DSeprnsj3WlGLGUZOSNopiS9vAJ8O3mELgOk6xdYxHI= +github.com/aws/aws-sdk-go-v2/service/route53 v1.40.0 h1:MRriK+ntpKpUc8RwcYJbc5W/eLfRV8MGFTYEcZe/QbU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.40.0/go.mod h1:n6oZO1BbhPw2X46ObAjn8ol00kujRT+Y+Q9AnbrRUe0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0 h1:rNVsCe3bqTAhG+qjnHJKgYKdHEsqqo/GMK3gEYY8W6g= +github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0= +github.com/aws/aws-sdk-go-v2/service/sqs v1.31.0 h1:QpCpvy+60VQ8BeIoQRwNA+sUGQr7fZxgF7B151RVMxw= +github.com/aws/aws-sdk-go-v2/service/sqs v1.31.0/go.mod h1:WBcfcQFNtBlD+ACJ0hpIxB6tPkee5RKXndXaVQ0WyhQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 h1:6YL8G91QZ52KlPrLkEgEez5kejIVwChVCgND3qgY5j0= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.0/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 h1:+DqIa5Ll7W311QLUvGFDdVit9uC4G0VioDdw08cXcow= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 h1:F7tQr61zYnTaeY50Rn4jwfVQbtcqJuBRwN/nGGNwzb0= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.0/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE= +github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= +github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= +github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= +github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= +github.com/charmbracelet/huh/spinner v0.0.0-20240209193029-45947515c4cf h1:hzfl5rHblaVR/8zfAoCBuqsTcEp/Zvy1IVZBIebZelM= +github.com/charmbracelet/huh/spinner v0.0.0-20240209193029-45947515c4cf/go.mod h1:2l0nupcBRhdSZQqIiaV2hKwctrYbBbOr9Dn6Smox3f4= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -21,6 +128,8 @@ github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -30,19 +139,30 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= 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/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jedib0t/go-pretty/v6 v6.5.4 h1:gOGo0613MoqUcf0xCj+h/V3sHDaZasfv152G6/5l91s= github.com/jedib0t/go-pretty/v6 v6.5.4/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -55,34 +175,81 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= +github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +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/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/nats-io/jwt/v2 v2.5.5 h1:ROfXb50elFq5c9+1ztaUbdlrArNFl2+fQWP6B8HGEq4= +github.com/nats-io/jwt/v2 v2.5.5/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= +github.com/nats-io/nats-server/v2 v2.10.10 h1:g1Wd64J5SGsoqWSx1qoNu9/At7a2x+jE7Qtf2XpEx/I= +github.com/nats-io/nats-server/v2 v2.10.10/go.mod h1:/TE61Dos8NlwZnjzyE3ZlOnM6dgl7tf937dnf4VclrA= github.com/nats-io/nats.go v1.32.0 h1:Bx9BZS+aXYlxW08k8Gd3yR2s73pV5XSoAQUyp1Kwvp0= github.com/nats-io/nats.go v1.32.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/openrdap/rdap v0.9.2-0.20240219192926-2210b535d40b h1:sM/kMuT+cm9hsSaZGVkGgwKHpQN3Et5/DF9zX/02X6w= +github.com/openrdap/rdap v0.9.2-0.20240219192926-2210b535d40b/go.mod h1:391Ww1JbjG4FHOlvQqCd6n25CCCPE64JzC5cCYPxhyM= +github.com/overmindtech/api-client v0.14.0 h1:zXyjJsIeawNqoWv7FqOjwcqgFpLrDYz7l9MWqh1G9ZQ= +github.com/overmindtech/api-client v0.14.0/go.mod h1:msdkTAQFlvDGOU4tQk2adk2P8j23uaMWkJ9YRX4wGWI= +github.com/overmindtech/aws-source v0.0.0-20240228174133-d45f6f687ef8 h1:wali9PO5lsWW+pnBT3UwbPyicLalFqBUPM3oC7hI2WI= +github.com/overmindtech/aws-source v0.0.0-20240228174133-d45f6f687ef8/go.mod h1:FUsP813VAeleGwWzoRTC64tims9FlRZ/+52Ifit3AF4= +github.com/overmindtech/discovery v0.26.3 h1:FLXfyCq36XaD6mF6D9OriHnFqwidLQq5WnTxaJpgg/c= +github.com/overmindtech/discovery v0.26.3/go.mod h1:k0E4UtBa8Fvv1LbQ6zb/w3IZZh8KY85HS3V6vS6Ab0Y= github.com/overmindtech/sdp-go v0.66.2 h1:NUW1lvx9uJekjRsgFgJdKUBOD1Jy2Ahbe+JRl84mgHQ= github.com/overmindtech/sdp-go v0.66.2/go.mod h1:p8+DRx17Og086v9NeACpoFO4vbpThozyROzECPIxFSY= +github.com/overmindtech/sdpcache v1.6.3 h1:Uf6xrh6cKdDInvTnIVBtfdb7rujrAVtZW4ySK0VYtbU= +github.com/overmindtech/sdpcache v1.6.3/go.mod h1:bH+Yo40dS6u5DUW2mlcaepWS/4q0sguZ92IOPgAMp9U= +github.com/overmindtech/stdlib-source v0.0.0-20240228111344-75ee76fc53e7 h1:tl/DLU0wFMQSrhf+R+J5SxIwc/+yyeZNOhU1fvB6h50= +github.com/overmindtech/stdlib-source v0.0.0-20240228111344-75ee76fc53e7/go.mod h1:ce4yxC6XffHnxXrudpgC47+1kfidGMKD8IYChtu3R6Q= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -108,6 +275,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -122,12 +290,19 @@ github.com/uptrace/opentelemetry-go-extra/otellogrus v0.2.3 h1:m5eNyOhch/7tyK6aN github.com/uptrace/opentelemetry-go-extra/otellogrus v0.2.3/go.mod h1:APPUXm9BbpH7NFkfpbw04raZSitzl19/3NOCu0rbI4E= github.com/uptrace/opentelemetry-go-extra/otelutil v0.2.3 h1:LyGS9cIZV0YVhE81zwfMhIE2l2flcj3wn5IoK4VkbWA= github.com/uptrace/opentelemetry-go-extra/otelutil v0.2.3/go.mod h1:RvCYhPchLhvQ9l9C9goblbgO7BaKt597kBMf5mgKyo0= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b h1:ajy6PPLDeQaf7xf4P/4Ie/wsUTEqjy3Irl+xFelmjk0= github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b/go.mod h1:TkoiLoIgvAxmagjbnKWq18F2VlqnIcqAx/HzmFAqXNU= github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw= github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s= github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= +github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= go.opentelemetry.io/contrib/detectors/aws/ec2 v1.24.0 h1:mdgKu9aAl3uNPmFFYhJoEhtnAEbpOiUEm672moYowTs= go.opentelemetry.io/contrib/detectors/aws/ec2 v1.24.0/go.mod h1:pLGyW++QzweLK2DV29J9LeZf01n7nEL+y7Yaxg5JA/k= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= @@ -142,6 +317,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/schema v0.0.7 h1:UslTvUtbFGmaxlpL1Z+aROrjsP7BRWt06Xxm5Ng/kAU= +go.opentelemetry.io/otel/schema v0.0.7/go.mod h1:jFb7hFFzdtEQ8R8HdbDGy4KuBctXNZwH1XJBP470kH4= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= @@ -153,15 +330,20 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= @@ -171,25 +353,35 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=