diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 28f131a1..4d9f6066 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,11 +13,13 @@ builds: - linux - windows - darwin + binary: overmind archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of uname. name_template: >- + {{ .Binary }}_ {{ .ProjectName }}_ {{- .Version }}_ {{- title .Os }}_ diff --git a/README.md b/README.md index 3d449052..97a966b7 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,31 @@ -# ovm-cli +# Overmind CLI -CLI to interact with the overmind API +CLI to interact with the Overmind API ``` Usage: - ovm-cli [command] - -Available Commands: - completion Generate the autocompletion script for the specified shell - create-bookmark Creates a bookmark from JSON. - create-invite Create a new invite - end-change Finishes the specified change. Call this just after you finished the change. This will store a snapshot of the current system state for later reference. - get-affected-bookmarks Calculates the bookmarks that would be overlapping with a snapshot. - get-bookmark Displays the contents of a bookmark. - get-change Displays the contents of a change. - get-snapshot Displays the contents of a snapshot. - help Help about any command - list-changes Displays the contents of a change. - list-invites List all invites - manual-change Creates a new Change from a given query - request Runs a request against the overmind API - revoke-invites Revoke an existing invite - start-change Starts the specified change. Call this just before you're about to start the change. This will store a snapshot of the current system state for later reference. - submit-plan Creates a new Change from a given terraform plan file + overmind [command] + +Infrastructure as Code: + terraform Run Terrafrom with Overmind's change tracking - COMING SOON + +Overmind API: + bookmarks Interact with the bookarks that were created in the Explore view + changes Create, update and delete changes in Overmind + invites Manage invites for your team to Overmind + request Runs a request against the overmind API + snapshots Create, view and delete snapshots if your infrastructure + +Additional Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command Flags: - --api-key string The API key to use for authentication, also read from OVM_API_KEY environment variable - --api-key-url string The overmind API Keys endpoint (defaults to --url) - --auth0-client-id string OAuth Client ID to use when connecting with auth (default "j3LylZtIosVPZtouKI8WuVHmE6Lluva1") - --auth0-domain string Auth0 domain to connect to (default "om-prod.eu.auth0.com") - --gateway-url string The overmind Gateway endpoint (defaults to /api/gateway on --url) - -h, --help help for ovm-cli - --honeycomb-api-key string If specified, configures opentelemetry libraries to submit traces to honeycomb. This requires --otel to be set. - --json-log Set to true to emit logs as json for easier parsing. - --log string Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace (default "info") - --otel If specified, configures opentelemetry and - optionally, see --sentry-dsn - sentry using their default environment configs. - --run-mode string Set the run mode for this service, 'release', 'debug' or 'test'. Defaults to 'release'. (default "release") - --sentry-dsn string If specified, configures sentry libraries to capture errors. This requires --otel to be set. - --stdout-trace-dump Dump all otel traces to stdout for debugging. This requires --otel to be set. - --url string The overmind API endpoint (default "https://api.prod.overmind.tech") - -v, --version version for ovm-cli - -Use "ovm-cli [command] --help" for more information about a command. + -h, --help help for overmind + --log string Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace (default "info") + -v, --version version for overmind + +Use "overmind [command] --help" for more information about a command. ``` ## Examples @@ -50,7 +34,7 @@ Upload a terraform plan to overmind for Blast Radius Analysis: ``` terraform show -json ./tfplan > ./tfplan.json -ovm-cli submit-plan --title "example change" ./tfplan1.json ./tfplan2.json ./tfplan3.json +overmind changes submit-plan --title "example change" ./tfplan1.json ./tfplan2.json ./tfplan3.json ``` ## Terraform ➡ Overmind Mapping diff --git a/cmd/auth_client.go b/cmd/auth_client.go index a53e5441..9b2865ef 100644 --- a/cmd/auth_client.go +++ b/cmd/auth_client.go @@ -16,24 +16,16 @@ import ( // embedded in the context and otel instrumentation func AuthenticatedApiKeyClient(ctx context.Context) sdpconnect.ApiKeyServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("api-key-url") - if url == "" { - url = viper.GetString("url") - viper.Set("api-key-url", url) - } - log.WithContext(ctx).WithField("api-key-url", url).Debug("Connecting to overmind apikeys API (pre-authenticated)") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind apikeys API (pre-authenticated)") return sdpconnect.NewApiKeyServiceClient(httpClient, url) } // UnauthenticatedApiKeyClient Returns an apikey client with otel instrumentation // but no authentication. Can only be used for ExchangeKeyForToken func UnauthenticatedApiKeyClient(ctx context.Context) sdpconnect.ApiKeyServiceClient { - url := viper.GetString("api-key-url") - if url == "" { - url = viper.GetString("url") - viper.Set("api-key-url", url) - } - log.WithContext(ctx).WithField("api-key-url", url).Debug("Connecting to overmind apikeys API") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind apikeys API") return sdpconnect.NewApiKeyServiceClient(otelhttp.DefaultClient, url) } @@ -41,12 +33,8 @@ func UnauthenticatedApiKeyClient(ctx context.Context) sdpconnect.ApiKeyServiceCl // embedded in the context and otel instrumentation func AuthenticatedBookmarkClient(ctx context.Context) sdpconnect.BookmarksServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("bookmark-url") - if url == "" { - url = viper.GetString("url") - viper.Set("bookmark-url", url) - } - log.WithContext(ctx).WithField("bookmark-url", url).Debug("Connecting to overmind bookmark API") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind bookmark API") return sdpconnect.NewBookmarksServiceClient(httpClient, url) } @@ -54,12 +42,8 @@ func AuthenticatedBookmarkClient(ctx context.Context) sdpconnect.BookmarksServic // embedded in the context and otel instrumentation func AuthenticatedChangesClient(ctx context.Context) sdpconnect.ChangesServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("changes-url") - if url == "" { - url = viper.GetString("url") - viper.Set("changes-url", url) - } - log.WithContext(ctx).WithField("changes-url", url).Debug("Connecting to overmind changes API") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind changes API") return sdpconnect.NewChangesServiceClient(httpClient, url) } @@ -67,12 +51,8 @@ func AuthenticatedChangesClient(ctx context.Context) sdpconnect.ChangesServiceCl // embedded in the context and otel instrumentation func AuthenticatedManagementClient(ctx context.Context) sdpconnect.ManagementServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("management-url") - if url == "" { - url = viper.GetString("url") - viper.Set("management-url", url) - } - log.WithContext(ctx).WithField("management-url", url).Debug("Connecting to overmind management API") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind management API") return sdpconnect.NewManagementServiceClient(httpClient, url) } @@ -80,12 +60,8 @@ func AuthenticatedManagementClient(ctx context.Context) sdpconnect.ManagementSer // embedded in the context and otel instrumentation func AuthenticatedSnapshotsClient(ctx context.Context) sdpconnect.SnapshotsServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("snapshot-url") - if url == "" { - url = viper.GetString("url") - viper.Set("snapshot-url", url) - } - log.WithContext(ctx).WithField("snapshot-url", url).Debug("Connecting to overmind snapshot API") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind snapshot API") return sdpconnect.NewSnapshotsServiceClient(httpClient, url) } @@ -93,12 +69,8 @@ func AuthenticatedSnapshotsClient(ctx context.Context) sdpconnect.SnapshotsServi // embedded in the context and otel instrumentation func AuthenticatedInviteClient(ctx context.Context) sdpconnect.InviteServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("invite-url") - if url == "" { - url = viper.GetString("url") - viper.Set("invite-url", url) - } - log.WithContext(ctx).WithField("invite-url", url).Debug("Connecting to overmind invite API") + url := viper.GetString("url") + log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind invite API") return sdpconnect.NewInviteServiceClient(httpClient, url) } diff --git a/cmd/bookmarks.go b/cmd/bookmarks.go new file mode 100644 index 00000000..38981f43 --- /dev/null +++ b/cmd/bookmarks.go @@ -0,0 +1,36 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// bookmarksCmd represents the bookmarks command +var bookmarksCmd = &cobra.Command{ + Use: "bookmarks", + GroupID: "api", + Short: "Interact with the bookarks that were created in the Explore view", + Long: `A bookmark in Overmind is a set of queries that are stored together and can be +executed as a single block.`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(bookmarksCmd) + + addAPIFlags(bookmarksCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // bookmarksCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // bookmarksCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/createbookmark.go b/cmd/bookmarks_create_bookmark.go similarity index 91% rename from cmd/createbookmark.go rename to cmd/bookmarks_create_bookmark.go index cd4e93d8..586a5aa4 100644 --- a/cmd/createbookmark.go +++ b/cmd/bookmarks_create_bookmark.go @@ -12,7 +12,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -108,7 +108,7 @@ func CreateBookmark(ctx context.Context, ready chan bool) int { }) if err != nil { log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "bookmark-url": viper.GetString("bookmark-url"), + "url": viper.GetString("url"), }).Error("failed to get bookmark") return 1 } @@ -140,11 +140,7 @@ func CreateBookmark(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(createBookmarkCmd) - - createBookmarkCmd.PersistentFlags().String("bookmark-url", "", "The bookmark service API endpoint (defaults to --url)") + bookmarksCmd.AddCommand(createBookmarkCmd) createBookmarkCmd.PersistentFlags().String("file", "", "JSON formatted file to read bookmark. (defaults to stdin)") - - createBookmarkCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") } diff --git a/cmd/getaffectedbookmarks.go b/cmd/bookmarks_get_affected_bookmarks.go similarity index 87% rename from cmd/getaffectedbookmarks.go rename to cmd/bookmarks_get_affected_bookmarks.go index 250ba769..5c00541a 100644 --- a/cmd/getaffectedbookmarks.go +++ b/cmd/bookmarks_get_affected_bookmarks.go @@ -10,7 +10,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -102,7 +102,7 @@ func GetAffectedBookmarks(ctx context.Context, ready chan bool) int { }) if err != nil { log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "bookmark-url": viper.GetString("bookmark-url"), + "url": viper.GetString("url"), }).Error("failed to get affected bookmarks") return 1 } @@ -116,13 +116,8 @@ func GetAffectedBookmarks(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(getAffectedBookmarksCmd) - - getAffectedBookmarksCmd.PersistentFlags().String("bookmark-url", "", "The bookmark service API endpoint (defaults to --url)") - getAffectedBookmarksCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") + bookmarksCmd.AddCommand(getAffectedBookmarksCmd) getAffectedBookmarksCmd.PersistentFlags().String("snapshot-uuid", "", "The UUID of the snapshot that should be checked.") getAffectedBookmarksCmd.PersistentFlags().String("bookmark-uuids", "", "A comma separated list of UUIDs of the potentially affected bookmarks.") - - getAffectedBookmarksCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") } diff --git a/cmd/getbookmark.go b/cmd/bookmarks_get_bookmark.go similarity index 90% rename from cmd/getbookmark.go rename to cmd/bookmarks_get_bookmark.go index 1964645c..57aa390c 100644 --- a/cmd/getbookmark.go +++ b/cmd/bookmarks_get_bookmark.go @@ -11,7 +11,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -91,7 +91,7 @@ func GetBookmark(ctx context.Context, ready chan bool) int { }) if err != nil { log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "bookmark-url": viper.GetString("bookmark-url"), + "url": viper.GetString("url"), }).Error("failed to get bookmark") return 1 } @@ -113,11 +113,7 @@ func GetBookmark(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(getBookmarkCmd) - - getBookmarkCmd.PersistentFlags().String("bookmark-url", "", "The bookmark service API endpoint (defaults to --url)") + bookmarksCmd.AddCommand(getBookmarkCmd) getBookmarkCmd.PersistentFlags().String("uuid", "", "The UUID of the bookmark that should be displayed.") - - getBookmarkCmd.PersistentFlags().String("timeout", "1m", "How long to wait for responses") } diff --git a/cmd/changes.go b/cmd/changes.go new file mode 100644 index 00000000..ac0cbb22 --- /dev/null +++ b/cmd/changes.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// changesCmd represents the changes command +var changesCmd = &cobra.Command{ + Use: "changes", + GroupID: "api", + Short: "Create, update and delete changes in Overmind", + Long: `Manage changes that are being tracked using Overmind. NOTE: It is probably +easier to use our IaC wrappers such as 'overmind terraform plan' rather than +using these commands directly, but they are provided for flexibility.`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(changesCmd) + + addAPIFlags(changesCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // changesCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // changesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/endchange.go b/cmd/changes_end_change.go similarity index 90% rename from cmd/endchange.go rename to cmd/changes_end_change.go index 815d93b9..de02b6d2 100644 --- a/cmd/endchange.go +++ b/cmd/changes_end_change.go @@ -9,7 +9,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -114,11 +114,7 @@ func EndChange(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(endChangeCmd) + changesCmd.AddCommand(endChangeCmd) - withChangeUuidFlags(endChangeCmd) - - endChangeCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") - - endChangeCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") + addChangeUuidFlags(endChangeCmd) } diff --git a/cmd/getchange.go b/cmd/changes_get_change.go similarity index 97% rename from cmd/getchange.go rename to cmd/changes_get_change.go index a3be68f3..b4046b26 100644 --- a/cmd/getchange.go +++ b/cmd/changes_get_change.go @@ -17,7 +17,7 @@ import ( "github.com/hexops/gotextdiff" "github.com/hexops/gotextdiff/myers" diffspan "github.com/hexops/gotextdiff/span" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -245,7 +245,7 @@ fetch: BlastItems: int(changeRes.Msg.GetChange().GetMetadata().GetNumAffectedItems()), BlastEdges: int(changeRes.Msg.GetChange().GetMetadata().GetNumAffectedEdges()), Risks: []TemplateRisk{}, - AssetPath: fmt.Sprintf("https://raw.githubusercontent.com/overmindtech/ovm-cli/%v/assets", assetVersion), + AssetPath: fmt.Sprintf("https://raw.githubusercontent.com/overmindtech/cli/%v/assets", assetVersion), } for _, item := range changeRes.Msg.GetChange().GetProperties().GetPlannedChanges() { @@ -324,13 +324,11 @@ fetch: } func init() { - rootCmd.AddCommand(getChangeCmd) + changesCmd.AddCommand(getChangeCmd) - withChangeUuidFlags(getChangeCmd) + addChangeUuidFlags(getChangeCmd) getChangeCmd.PersistentFlags().String("status", "", "The expected status of the change. Use this with --ticket-link. Allowed values: CHANGE_STATUS_UNSPECIFIED, CHANGE_STATUS_DEFINING, CHANGE_STATUS_HAPPENING, CHANGE_STATUS_PROCESSING, CHANGE_STATUS_DONE") getChangeCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") getChangeCmd.PersistentFlags().String("format", "json", "How to render the change. Possible values: json, markdown") - - getChangeCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") } diff --git a/cmd/listchanges.go b/cmd/changes_list_changes.go similarity index 96% rename from cmd/listchanges.go rename to cmd/changes_list_changes.go index d2d570b6..b000585a 100644 --- a/cmd/listchanges.go +++ b/cmd/changes_list_changes.go @@ -12,7 +12,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -287,12 +287,9 @@ func printJson(ctx context.Context, b []byte, prefix, id string) error { } func init() { - rootCmd.AddCommand(listChangesCmd) + changesCmd.AddCommand(listChangesCmd) - listChangesCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") listChangesCmd.PersistentFlags().String("format", "files", "How to render the change. Possible values: files, json") listChangesCmd.PersistentFlags().String("dir", "./output", "A directory name to use for rendering changes when using the 'files' format") listChangesCmd.PersistentFlags().Bool("fetch-data", false, "also fetch the blast radius and system state snapshots for each change") - - listChangesCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") } diff --git a/cmd/manualchange.go b/cmd/changes_manual_change.go similarity index 90% rename from cmd/manualchange.go rename to cmd/changes_manual_change.go index 7679d600..72ec5140 100644 --- a/cmd/manualchange.go +++ b/cmd/changes_manual_change.go @@ -11,7 +11,8 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/internal" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" "github.com/overmindtech/sdp-go/sdpws" log "github.com/sirupsen/logrus" @@ -67,17 +68,11 @@ func ManualChange(ctx context.Context, ready chan bool) int { )) defer span.End() - gatewayUrl := viper.GetString("gateway-url") - if gatewayUrl == "" { - gatewayUrl = fmt.Sprintf("%v/api/gateway", viper.GetString("url")) - viper.Set("gateway-url", gatewayUrl) - } - lf := log.Fields{} ctx, err = ensureToken(ctx, []string{"changes:write"}) if err != nil { - log.WithContext(ctx).WithFields(lf).WithField("api-key-url", viper.GetString("api-key-url")).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -130,7 +125,7 @@ func ManualChange(ctx context.Context, ready chan bool) int { return 1 } - ws, err := sdpws.DialBatch(ctx, gatewayUrl, otelhttp.DefaultClient, nil) + ws, err := sdpws.DialBatch(ctx, internal.GatewayURL(viper.GetString("url")), otelhttp.DefaultClient, nil) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to connect to gateway") return 1 @@ -225,10 +220,8 @@ func ManualChange(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(manualChangeCmd) + changesCmd.AddCommand(manualChangeCmd) - manualChangeCmd.PersistentFlags().String("changes-url", "", "The changes service API endpoint (defaults to --url)") - manualChangeCmd.PersistentFlags().String("management-url", "", "The management service API endpoint (defaults to --url)") manualChangeCmd.PersistentFlags().String("frontend", "https://app.overmind.tech", "The frontend base URL") manualChangeCmd.PersistentFlags().String("title", "", "Short title for this change.") @@ -241,6 +234,4 @@ func init() { manualChangeCmd.PersistentFlags().String("query-scope", "*", "The scope to query") manualChangeCmd.PersistentFlags().String("query-type", "*", "The type to query") manualChangeCmd.PersistentFlags().String("query", "", "The actual query to send") - - manualChangeCmd.PersistentFlags().String("timeout", "3m", "How long to wait for responses") } diff --git a/cmd/submitplan.go b/cmd/changes_submit_plan.go similarity index 96% rename from cmd/submitplan.go rename to cmd/changes_submit_plan.go index 80cb28c5..3dfc5718 100644 --- a/cmd/submitplan.go +++ b/cmd/changes_submit_plan.go @@ -17,8 +17,8 @@ import ( "connectrpc.com/connect" "github.com/getsentry/sentry-go" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/cmd/datamaps" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/cmd/datamaps" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -564,17 +564,11 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { )) defer span.End() - gatewayUrl := viper.GetString("gateway-url") - if gatewayUrl == "" { - gatewayUrl = fmt.Sprintf("%v/api/gateway", viper.GetString("url")) - viper.Set("gateway-url", gatewayUrl) - } - lf := log.Fields{} ctx, err = ensureToken(ctx, []string{"changes:write"}) if err != nil { - log.WithContext(ctx).WithFields(lf).WithField("api-key-url", viper.GetString("api-key-url")).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -730,13 +724,11 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { } func init() { - rootCmd.AddCommand(submitPlanCmd) + changesCmd.AddCommand(submitPlanCmd) - submitPlanCmd.PersistentFlags().String("changes-url", "", "The changes service API endpoint (defaults to --url)") - submitPlanCmd.PersistentFlags().String("management-url", "", "The management service API endpoint (defaults to --url)") submitPlanCmd.PersistentFlags().String("frontend", "https://app.overmind.tech", "The frontend base URL") - submitPlanCmd.PersistentFlags().String("title", "", "Short title for this change. If this is not specified, ovm-cli will try to come up with one for you.") + submitPlanCmd.PersistentFlags().String("title", "", "Short title for this change. If this is not specified, overmind will try to come up with one for you.") submitPlanCmd.PersistentFlags().String("description", "", "Quick description of the change.") submitPlanCmd.PersistentFlags().String("ticket-link", "*", "Link to the ticket for this change.") submitPlanCmd.PersistentFlags().String("owner", "", "The owner of this change.") @@ -744,6 +736,4 @@ func init() { submitPlanCmd.PersistentFlags().String("terraform-plan-output", "", "Filename of cached terraform plan output for this change.") submitPlanCmd.PersistentFlags().String("code-changes-diff", "", "Fileame of the code diff of this change.") - - submitPlanCmd.PersistentFlags().String("timeout", "3m", "How long to wait for responses") } diff --git a/cmd/startchange.go b/cmd/chnages_start_change.go similarity index 90% rename from cmd/startchange.go rename to cmd/chnages_start_change.go index ca50b443..3f7b154e 100644 --- a/cmd/startchange.go +++ b/cmd/chnages_start_change.go @@ -9,7 +9,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -114,11 +114,7 @@ func StartChange(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(startChangeCmd) + changesCmd.AddCommand(startChangeCmd) - withChangeUuidFlags(startChangeCmd) - - startChangeCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") - - startChangeCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") + addChangeUuidFlags(startChangeCmd) } diff --git a/cmd/invites.go b/cmd/invites.go new file mode 100644 index 00000000..406f1d86 --- /dev/null +++ b/cmd/invites.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// invitesCmd represents the invites command +var invitesCmd = &cobra.Command{ + Use: "invites", + GroupID: "api", + Short: "Manage invites for your team to Overmind", + Long: `Create and revoke Overmind invitations`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(invitesCmd) + + addAPIFlags(invitesCmd) + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // invitesCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // invitesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/invite.go b/cmd/invites_crud.go similarity index 93% rename from cmd/invite.go rename to cmd/invites_crud.go index 886624d5..5351bd78 100644 --- a/cmd/invite.go +++ b/cmd/invites_crud.go @@ -9,7 +9,7 @@ import ( "connectrpc.com/connect" "github.com/jedib0t/go-pretty/v6/table" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -237,16 +237,13 @@ func InvitesList(ctx context.Context) int { func init() { // list sub-command - rootCmd.AddCommand(listCmd) - listCmd.PersistentFlags().String("invite-url", "", "A custom URL for the invites API (optional)") + invitesCmd.AddCommand(listCmd) // create sub-command - rootCmd.AddCommand(createCmd) - createCmd.PersistentFlags().String("invite-url", "", "A custom URL for the invites API (optional)") + invitesCmd.AddCommand(createCmd) createCmd.PersistentFlags().StringSlice("emails", []string{}, "A list of emails to invite") // revoke sub-command - rootCmd.AddCommand(revokeCmd) - revokeCmd.PersistentFlags().String("invite-url", "", "A custom URL for the invites API (optional)") + invitesCmd.AddCommand(revokeCmd) revokeCmd.PersistentFlags().String("email", "", "The email address to revoke") } diff --git a/cmd/request.go b/cmd/request.go index 631ced3d..6fe85342 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -10,7 +10,8 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/internal" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" "github.com/overmindtech/sdp-go/sdpws" log "github.com/sirupsen/logrus" @@ -24,8 +25,9 @@ import ( // requestCmd represents the start command var requestCmd = &cobra.Command{ - Use: "request", - Short: "Runs a request against the overmind API", + Use: "request", + GroupID: "api", + Short: "Runs a request against the overmind API", PreRun: func(cmd *cobra.Command, args []string) { // Bind these to viper err := viper.BindPFlags(cmd.Flags()) @@ -140,17 +142,11 @@ func Request(ctx context.Context, ready chan bool) int { )) defer span.End() - gatewayUrl := viper.GetString("gateway-url") - if gatewayUrl == "" { - gatewayUrl = fmt.Sprintf("%v/api/gateway", viper.GetString("url")) - viper.Set("gateway-url", gatewayUrl) - } - lf := log.Fields{} ctx, err = ensureToken(ctx, []string{"explore:read"}) if err != nil { - log.WithContext(ctx).WithFields(lf).WithField("api-key-url", viper.GetString("api-key-url")).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -165,6 +161,7 @@ func Request(ctx context.Context, ready chan bool) int { edges: []*sdp.Edge{}, msgLog: []*sdp.GatewayResponse{}, } + gatewayUrl := internal.GatewayURL(viper.GetString("url")) c, err := sdpws.DialBatch(ctx, gatewayUrl, NewAuthenticatedClient(ctx, otelhttp.DefaultClient), handler, diff --git a/cmd/root.go b/cmd/root.go index 4bec76ee..c75742dc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,7 +17,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -26,6 +26,9 @@ import ( "golang.org/x/oauth2" ) +const Auth0ClientId = "j3LylZtIosVPZtouKI8WuVHmE6Lluva1" +const Auth0Domain = "om-prod.eu.auth0.com" + var logLevel string //go:generate sh -c "echo -n $(git describe --tags --long) > commit.txt" @@ -34,9 +37,14 @@ var cliVersion string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "ovm-cli", - Short: "A CLI to interact with the overmind API", - Long: `The ovm-cli allows direct access to the overmind API`, + Use: "overmind", + Short: "The Overmind CLI", + Long: `Calculate the blast radius of your changes, track risks, and make changes with +confidence. + +This CLI will prompt you for authentication using Overmind's OAuth service, +however it can also be configured to use an API key by setting the OVM_API_KEY +environment variable.`, Version: cliVersion, PreRun: func(cmd *cobra.Command, args []string) { // Bind these to viper @@ -162,7 +170,7 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context, log.WithContext(ctx).Debug("successfully authenticated") apiKey = resp.Msg.GetAccessToken() } else { - return ctx, errors.New("--api-key does not match pattern 'ovm_api_*'") + return ctx, errors.New("OVM_API_KEY does not match pattern 'ovm_api_*'") } return context.WithValue(ctx, sdp.UserTokenContextKey{}, apiKey), nil } @@ -198,11 +206,11 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context, // Authenticate using the oauth resource owner password flow config := oauth2.Config{ - ClientID: viper.GetString("auth0-client-id"), + ClientID: Auth0ClientId, Scopes: requestScopes, Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf("https://%v/authorize", viper.GetString("auth0-domain")), - TokenURL: fmt.Sprintf("https://%v/oauth/token", viper.GetString("auth0-domain")), + AuthURL: fmt.Sprintf("https://%v/authorize", Auth0Domain), + TokenURL: fmt.Sprintf("https://%v/oauth/token", Auth0Domain), }, RedirectURL: "http://127.0.0.1:7837/oauth/callback", } @@ -327,7 +335,7 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context, // Set the token return context.WithValue(ctx, sdp.UserTokenContextKey{}, token.AccessToken), nil } - return ctx, fmt.Errorf("no --api-key configured and target URL (%v) is insecure", parsed) + return ctx, fmt.Errorf("no OVM_API_KEY configured and target URL (%v) is insecure", parsed) } // getChangeUuid returns the UUID of a change, as selected by --uuid or --change, or a state with the specified status and having --ticket-link @@ -405,42 +413,50 @@ func parseChangeUrl(changeUrlString string) (uuid.UUID, error) { return changeUuid, nil } -func withChangeUuidFlags(cmd *cobra.Command) { +func addChangeUuidFlags(cmd *cobra.Command) { cmd.PersistentFlags().String("change", "", "The frontend URL of the change to get") cmd.PersistentFlags().String("ticket-link", "", "Link to the ticket for this change.") cmd.PersistentFlags().String("uuid", "", "The UUID of the change that should be displayed.") cmd.MarkFlagsMutuallyExclusive("change", "ticket-link", "uuid") } +// Adds common flags to API commands e.g. timeout +func addAPIFlags(cmd *cobra.Command) { + cmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") + cmd.PersistentFlags().String("url", "https://api.prod.overmind.tech", "The overmind API endpoint") +} + func init() { cobra.OnInitialize(initConfig) // General Config rootCmd.PersistentFlags().StringVar(&logLevel, "log", "info", "Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace") - // api endpoint - rootCmd.PersistentFlags().String("url", "https://api.prod.overmind.tech", "The overmind API endpoint") - rootCmd.PersistentFlags().String("gateway-url", "", "The overmind Gateway endpoint (defaults to /api/gateway on --url)") - - // authorization - rootCmd.PersistentFlags().String("api-key", "", "The API key to use for authentication, also read from OVM_API_KEY environment variable") + // Support API Keys in the environment err := viper.BindEnv("api-key", "OVM_API_KEY", "API_KEY") if err != nil { log.WithError(err).Fatal("could not bind api key to env") } - rootCmd.PersistentFlags().String("api-key-url", "", "The overmind API Keys endpoint (defaults to --url)") - rootCmd.PersistentFlags().String("auth0-client-id", "j3LylZtIosVPZtouKI8WuVHmE6Lluva1", "OAuth Client ID to use when connecting with auth") - rootCmd.PersistentFlags().String("auth0-domain", "om-prod.eu.auth0.com", "Auth0 domain to connect to") // tracing - rootCmd.PersistentFlags().Bool("otel", false, "If specified, configures opentelemetry and - optionally, see --sentry-dsn - sentry using their default environment configs.") rootCmd.PersistentFlags().String("honeycomb-api-key", "", "If specified, configures opentelemetry libraries to submit traces to honeycomb. This requires --otel to be set.") - rootCmd.PersistentFlags().String("sentry-dsn", "", "If specified, configures sentry libraries to capture errors. This requires --otel to be set.") - rootCmd.PersistentFlags().String("run-mode", "release", "Set the run mode for this service, 'release', 'debug' or 'test'. Defaults to 'release'.") - rootCmd.PersistentFlags().Bool("json-log", false, "Set to true to emit logs as json for easier parsing.") + // Mark this as hidden. This means that it will still be parsed of supplied, + // and we will still look for it in the environment, but it won't be shown + // in the help + err = rootCmd.PersistentFlags().MarkHidden("honeycomb-api-key") + if err != nil { + log.WithError(err).Fatal("could not mark `honeycomb-api-key` flag as hidden") + } - // debugging - rootCmd.PersistentFlags().Bool("stdout-trace-dump", false, "Dump all otel traces to stdout for debugging. This requires --otel to be set.") + // Create groups + rootCmd.AddGroup(&cobra.Group{ + ID: "iac", + Title: "Infrastructure as Code:", + }) + rootCmd.AddGroup(&cobra.Group{ + ID: "api", + Title: "Overmind API:", + }) // Run this before we do anything to set up the loglevel rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { @@ -462,21 +478,20 @@ func init() { } log.SetLevel(lvl) - if viper.GetBool("json-log") { - log.SetFormatter(&log.JSONFormatter{}) - } + if honeycombApiKey := viper.GetString("honeycomb-api-key"); honeycombApiKey != "" { + if err := tracing.InitTracerWithHoneycomb(honeycombApiKey); err != nil { + log.Fatal(err) + } - if err := tracing.InitTracerWithHoneycomb(viper.GetString("honeycomb-api-key")); err != nil { - log.Fatal(err) - } + log.AddHook(otellogrus.NewHook(otellogrus.WithLevels( + log.AllLevels[:log.GetLevel()+1]..., + ))) - log.AddHook(otellogrus.NewHook(otellogrus.WithLevels( - log.AllLevels[:log.GetLevel()+1]..., - ))) - } - // shut down tracing at the end of the process - rootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) { - tracing.ShutdownTracer() + // shut down tracing at the end of the process + rootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) { + tracing.ShutdownTracer() + } + } } } diff --git a/cmd/snapshots.go b/cmd/snapshots.go new file mode 100644 index 00000000..8604c72f --- /dev/null +++ b/cmd/snapshots.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// snapshotsCmd represents the snapshots command +var snapshotsCmd = &cobra.Command{ + Use: "snapshots", + GroupID: "api", + Short: "Create, view and delete snapshots if your infrastructure", + Long: `Overmind automatically creates snapshots are part of the change lifecycle, +however you can use these commands to interact directly with the API if +required.`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(snapshotsCmd) + + addAPIFlags(snapshotsCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // snapshotsCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // snapshotsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/getsnapshot.go b/cmd/snapshots_get_snapshot.go similarity index 89% rename from cmd/getsnapshot.go rename to cmd/snapshots_get_snapshot.go index 77dfdd32..d22c6877 100644 --- a/cmd/getsnapshot.go +++ b/cmd/snapshots_get_snapshot.go @@ -11,7 +11,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/ovm-cli/tracing" + "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -91,7 +91,7 @@ func GetSnapshot(ctx context.Context, ready chan bool) int { }) if err != nil { log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "snapshot-url": viper.GetString("snapshot-url"), + "url": viper.GetString("url"), }).Error("failed to get snapshot") return 1 } @@ -128,12 +128,7 @@ func GetSnapshot(ctx context.Context, ready chan bool) int { } func init() { - rootCmd.AddCommand(getSnapshotCmd) - - getSnapshotCmd.PersistentFlags().String("snapshot-url", "", "The snapshot service API endpoint (defaults to --url)") - getSnapshotCmd.PersistentFlags().String("frontend", "https://app.overmind.tech/", "The frontend base URL") + snapshotsCmd.AddCommand(getSnapshotCmd) getSnapshotCmd.PersistentFlags().String("uuid", "", "The UUID of the snapshot that should be displayed.") - - getSnapshotCmd.PersistentFlags().String("timeout", "5m", "How long to wait for responses") } diff --git a/cmd/terraform.go b/cmd/terraform.go new file mode 100644 index 00000000..6f96fe0f --- /dev/null +++ b/cmd/terraform.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// terraformCmd represents the terraform command +var terraformCmd = &cobra.Command{ + Use: "terraform", + GroupID: "iac", + Short: "Run Terrafrom with Overmind's change tracking - COMING SOON", + Long: `COMING SOON`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(terraformCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // terraformCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // terraformCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/go.mod b/go.mod index 5ff9820c..85c1f980 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/overmindtech/ovm-cli +module github.com/overmindtech/cli go 1.22.0 diff --git a/internal/paths.go b/internal/paths.go new file mode 100644 index 00000000..7919fa0f --- /dev/null +++ b/internal/paths.go @@ -0,0 +1,10 @@ +package internal + +import "fmt" + +// GatewayURL returns the URL for the gateway for a given pase URL. For example +// if the base URL is https://api.prod.overmind.tech, the gateway URL will be +// https://api.prod.overmind.tech/api/gateway +func GatewayURL(base string) string { + return fmt.Sprintf("%v/api/gateway", base) +} diff --git a/main.go b/main.go index d4ba0778..27cce098 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,6 @@ package main -import "github.com/overmindtech/ovm-cli/cmd" +import "github.com/overmindtech/cli/cmd" func main() { cmd.Execute() diff --git a/tracing/main.go b/tracing/main.go index f04c95fd..ce3a3d3a 100644 --- a/tracing/main.go +++ b/tracing/main.go @@ -28,7 +28,7 @@ import ( //go:embed commit.txt var instrumentationVersion string -const instrumentationName = "github.com/overmindtech/ovm-cli" +const instrumentationName = "github.com/overmindtech/cli" var tracer = otel.GetTracerProvider().Tracer( instrumentationName, @@ -70,7 +70,7 @@ func tracingResource() *resource.Resource { resource.WithSchemaURL(semconv.SchemaURL), // Add your own custom attributes to identify your application resource.WithAttributes( - semconv.ServiceNameKey.String("ovm-cli"), + semconv.ServiceNameKey.String("overmind-cli"), semconv.ServiceVersionKey.String("0.0.1"), ), )