From 59e8c03500321b663d7e089d03a4fb17e911a1ce Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 27 Feb 2024 15:43:59 +0100 Subject: [PATCH] Implement a central endpoint to select Overmind Instance The new `--app` commandline flag is a one-stop-shop to set all instance-specific values from data in the frontend. This allows for a more consistent and less error-prone way to select the instance to work with. Notably this requires the user to specify a frontend URL, which is easily copy-pasted from the browser. --- cmd/auth_client.go | 50 ++++++-------- cmd/bookmarks_create_bookmark.go | 22 ++++--- cmd/bookmarks_get_affected_bookmarks.go | 22 ++++--- cmd/bookmarks_get_bookmark.go | 22 ++++--- cmd/changes_end_change.go | 21 ++++-- cmd/changes_get_change.go | 21 ++++-- cmd/changes_list_changes.go | 24 ++++--- cmd/changes_manual_change.go | 19 ++++-- cmd/changes_start_change.go | 21 ++++-- cmd/changes_submit_plan.go | 15 +++-- cmd/invites_crud.go | 46 ++++++++++--- cmd/request.go | 16 +++-- cmd/root.go | 87 ++++++++++++++++++++++--- cmd/snapshots_get_snapshot.go | 22 ++++--- internal/paths.go | 10 --- 15 files changed, 285 insertions(+), 133 deletions(-) delete mode 100644 internal/paths.go diff --git a/cmd/auth_client.go b/cmd/auth_client.go index 9b2865ef..764ef4c2 100644 --- a/cmd/auth_client.go +++ b/cmd/auth_client.go @@ -8,70 +8,62 @@ import ( "github.com/overmindtech/sdp-go" "github.com/overmindtech/sdp-go/sdpconnect" log "github.com/sirupsen/logrus" - "github.com/spf13/viper" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) // AuthenticatedApiKeyClient Returns an apikey client that uses the auth // embedded in the context and otel instrumentation -func AuthenticatedApiKeyClient(ctx context.Context) sdpconnect.ApiKeyServiceClient { +func AuthenticatedApiKeyClient(ctx context.Context, oi OvermindInstance) sdpconnect.ApiKeyServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind apikeys API (pre-authenticated)") - return sdpconnect.NewApiKeyServiceClient(httpClient, url) + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind apikeys API (pre-authenticated)") + return sdpconnect.NewApiKeyServiceClient(httpClient, oi.ApiUrl.String()) } // 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("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind apikeys API") - return sdpconnect.NewApiKeyServiceClient(otelhttp.DefaultClient, url) +func UnauthenticatedApiKeyClient(ctx context.Context, oi OvermindInstance) sdpconnect.ApiKeyServiceClient { + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind apikeys API") + return sdpconnect.NewApiKeyServiceClient(otelhttp.DefaultClient, oi.ApiUrl.String()) } // AuthenticatedBookmarkClient Returns a bookmark client that uses the auth // embedded in the context and otel instrumentation -func AuthenticatedBookmarkClient(ctx context.Context) sdpconnect.BookmarksServiceClient { +func AuthenticatedBookmarkClient(ctx context.Context, oi OvermindInstance) sdpconnect.BookmarksServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind bookmark API") - return sdpconnect.NewBookmarksServiceClient(httpClient, url) + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind bookmark API") + return sdpconnect.NewBookmarksServiceClient(httpClient, oi.ApiUrl.String()) } // AuthenticatedChangesClient Returns a bookmark client that uses the auth // embedded in the context and otel instrumentation -func AuthenticatedChangesClient(ctx context.Context) sdpconnect.ChangesServiceClient { +func AuthenticatedChangesClient(ctx context.Context, oi OvermindInstance) sdpconnect.ChangesServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind changes API") - return sdpconnect.NewChangesServiceClient(httpClient, url) + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind changes API") + return sdpconnect.NewChangesServiceClient(httpClient, oi.ApiUrl.String()) } // AuthenticatedManagementClient Returns a bookmark client that uses the auth // embedded in the context and otel instrumentation -func AuthenticatedManagementClient(ctx context.Context) sdpconnect.ManagementServiceClient { +func AuthenticatedManagementClient(ctx context.Context, oi OvermindInstance) sdpconnect.ManagementServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind management API") - return sdpconnect.NewManagementServiceClient(httpClient, url) + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind management API") + return sdpconnect.NewManagementServiceClient(httpClient, oi.ApiUrl.String()) } // AuthenticatedSnapshotsClient Returns a Snapshots client that uses the auth // embedded in the context and otel instrumentation -func AuthenticatedSnapshotsClient(ctx context.Context) sdpconnect.SnapshotsServiceClient { +func AuthenticatedSnapshotsClient(ctx context.Context, oi OvermindInstance) sdpconnect.SnapshotsServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind snapshot API") - return sdpconnect.NewSnapshotsServiceClient(httpClient, url) + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind snapshot API") + return sdpconnect.NewSnapshotsServiceClient(httpClient, oi.ApiUrl.String()) } // AuthenticatedInviteClient Returns a Invite client that uses the auth // embedded in the context and otel instrumentation -func AuthenticatedInviteClient(ctx context.Context) sdpconnect.InviteServiceClient { +func AuthenticatedInviteClient(ctx context.Context, oi OvermindInstance) sdpconnect.InviteServiceClient { httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient) - url := viper.GetString("url") - log.WithContext(ctx).WithField("url", url).Debug("Connecting to overmind invite API") - return sdpconnect.NewInviteServiceClient(httpClient, url) + log.WithContext(ctx).WithField("apiUrl", oi.ApiUrl).Debug("Connecting to overmind invite API") + return sdpconnect.NewInviteServiceClient(httpClient, oi.ApiUrl.String()) } // AuthenticatedClient is a http.Client that will automatically add the required diff --git a/cmd/bookmarks_create_bookmark.go b/cmd/bookmarks_create_bookmark.go index 586a5aa4..bb4e8c38 100644 --- a/cmd/bookmarks_create_bookmark.go +++ b/cmd/bookmarks_create_bookmark.go @@ -77,11 +77,19 @@ func CreateBookmark(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:write"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + 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"}) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to authenticate") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 } @@ -100,16 +108,14 @@ func CreateBookmark(ctx context.Context, ready chan bool) int { log.WithContext(ctx).WithError(err).Error("failed to parse input") return 1 } - client := AuthenticatedBookmarkClient(ctx) + client := AuthenticatedBookmarkClient(ctx, oi) response, err := client.CreateBookmark(ctx, &connect.Request[sdp.CreateBookmarkRequest]{ Msg: &sdp.CreateBookmarkRequest{ Properties: &msg, }, }) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to get bookmark") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get bookmark") return 1 } log.WithContext(ctx).WithFields(log.Fields{ diff --git a/cmd/bookmarks_get_affected_bookmarks.go b/cmd/bookmarks_get_affected_bookmarks.go index 5c00541a..e01f7d7d 100644 --- a/cmd/bookmarks_get_affected_bookmarks.go +++ b/cmd/bookmarks_get_affected_bookmarks.go @@ -81,11 +81,19 @@ func GetAffectedBookmarks(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:read"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + + ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to authenticate") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 } @@ -93,7 +101,7 @@ func GetAffectedBookmarks(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - client := AuthenticatedBookmarkClient(ctx) + client := AuthenticatedBookmarkClient(ctx, oi) response, err := client.GetAffectedBookmarks(ctx, &connect.Request[sdp.GetAffectedBookmarksRequest]{ Msg: &sdp.GetAffectedBookmarksRequest{ SnapshotUUID: snapshotUuid[:], @@ -101,9 +109,7 @@ func GetAffectedBookmarks(ctx context.Context, ready chan bool) int { }, }) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to get affected bookmarks") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get affected bookmarks") return 1 } for _, u := range response.Msg.GetBookmarkUUIDs() { diff --git a/cmd/bookmarks_get_bookmark.go b/cmd/bookmarks_get_bookmark.go index 57aa390c..802b3d0a 100644 --- a/cmd/bookmarks_get_bookmark.go +++ b/cmd/bookmarks_get_bookmark.go @@ -71,11 +71,19 @@ func GetBookmark(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:read"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + + ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to authenticate") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 } @@ -83,16 +91,14 @@ func GetBookmark(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - client := AuthenticatedBookmarkClient(ctx) + client := AuthenticatedBookmarkClient(ctx, oi) response, err := client.GetBookmark(ctx, &connect.Request[sdp.GetBookmarkRequest]{ Msg: &sdp.GetBookmarkRequest{ UUID: bookmarkUuid[:], }, }) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to get bookmark") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get bookmark") return 1 } log.WithContext(ctx).WithFields(log.Fields{ diff --git a/cmd/changes_end_change.go b/cmd/changes_end_change.go index de02b6d2..2a7a9331 100644 --- a/cmd/changes_end_change.go +++ b/cmd/changes_end_change.go @@ -63,11 +63,19 @@ func EndChange(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:write"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + 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"}) if err != nil { - log.WithContext(ctx).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -75,8 +83,7 @@ func EndChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - lf := log.Fields{} - changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, true) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, true) if err != nil { log.WithError(err).WithFields(lf).Error("failed to identify change") return 1 @@ -85,7 +92,7 @@ func EndChange(ctx context.Context, ready chan bool) int { lf["uuid"] = changeUuid.String() // snapClient := AuthenticatedSnapshotsClient(ctx) - client := AuthenticatedChangesClient(ctx) + client := AuthenticatedChangesClient(ctx, oi) stream, err := client.EndChange(ctx, &connect.Request[sdp.EndChangeRequest]{ Msg: &sdp.EndChangeRequest{ ChangeUUID: changeUuid[:], diff --git a/cmd/changes_get_change.go b/cmd/changes_get_change.go index 438ba407..fadab607 100644 --- a/cmd/changes_get_change.go +++ b/cmd/changes_get_change.go @@ -82,11 +82,19 @@ func GetChange(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:read"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + + ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { - log.WithContext(ctx).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -94,8 +102,7 @@ func GetChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - lf := log.Fields{} - changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus(sdp.ChangeStatus_value[viper.GetString("status")]), true) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus(sdp.ChangeStatus_value[viper.GetString("status")]), true) if err != nil { log.WithError(err).WithFields(lf).Error("failed to identify change") return 1 @@ -103,7 +110,7 @@ func GetChange(ctx context.Context, ready chan bool) int { lf["uuid"] = changeUuid.String() - client := AuthenticatedChangesClient(ctx) + client := AuthenticatedChangesClient(ctx, oi) var riskRes *connect.Response[sdp.GetChangeRisksResponse] fetch: for { diff --git a/cmd/changes_list_changes.go b/cmd/changes_list_changes.go index b000585a..ece1a95e 100644 --- a/cmd/changes_list_changes.go +++ b/cmd/changes_list_changes.go @@ -66,11 +66,19 @@ func ListChanges(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:read"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) if err != nil { - log.WithContext(ctx).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + + ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) + if err != nil { + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -78,15 +86,15 @@ func ListChanges(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - snapshots := AuthenticatedSnapshotsClient(ctx) - bookmarks := AuthenticatedBookmarkClient(ctx) - changes := AuthenticatedChangesClient(ctx) + snapshots := AuthenticatedSnapshotsClient(ctx, oi) + bookmarks := AuthenticatedBookmarkClient(ctx, oi) + changes := AuthenticatedChangesClient(ctx, oi) response, err := changes.ListChanges(ctx, &connect.Request[sdp.ListChangesRequest]{ Msg: &sdp.ListChangesRequest{}, }) if err != nil { - log.WithContext(ctx).WithError(err).Error("failed to list changes") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to list changes") return 1 } for _, change := range response.Msg.GetChanges() { diff --git a/cmd/changes_manual_change.go b/cmd/changes_manual_change.go index 72ec5140..51255062 100644 --- a/cmd/changes_manual_change.go +++ b/cmd/changes_manual_change.go @@ -11,7 +11,6 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/internal" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" "github.com/overmindtech/sdp-go/sdpws" @@ -68,9 +67,17 @@ func ManualChange(ctx context.Context, ready chan bool) int { )) defer span.End() - lf := log.Fields{} + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } - ctx, err = ensureToken(ctx, []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 @@ -80,8 +87,8 @@ func ManualChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - client := AuthenticatedChangesClient(ctx) - changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) + client := AuthenticatedChangesClient(ctx, oi) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to searching for existing changes") return 1 @@ -125,7 +132,7 @@ func ManualChange(ctx context.Context, ready chan bool) int { return 1 } - ws, err := sdpws.DialBatch(ctx, internal.GatewayURL(viper.GetString("url")), otelhttp.DefaultClient, nil) + ws, err := sdpws.DialBatch(ctx, oi.GatewayUrl(), otelhttp.DefaultClient, nil) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to connect to gateway") return 1 diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index 3f7b154e..daf04c2e 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -63,11 +63,19 @@ func StartChange(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:write"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + 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"}) if err != nil { - log.WithContext(ctx).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).WithError(err).Error("failed to authenticate") + log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 } @@ -75,8 +83,7 @@ func StartChange(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - lf := log.Fields{} - changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, true) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, true) if err != nil { log.WithError(err).WithFields(lf).Error("failed to identify change") return 1 @@ -85,7 +92,7 @@ func StartChange(ctx context.Context, ready chan bool) int { lf["uuid"] = changeUuid.String() // snapClient := AuthenticatedSnapshotsClient(ctx) - client := AuthenticatedChangesClient(ctx) + client := AuthenticatedChangesClient(ctx, oi) stream, err := client.StartChange(ctx, &connect.Request[sdp.StartChangeRequest]{ Msg: &sdp.StartChangeRequest{ ChangeUUID: changeUuid[:], diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index 2ed0d07d..f433a47d 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -580,9 +580,16 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { )) defer span.End() - lf := log.Fields{} + lf := log.Fields{ + "app": viper.GetString("app"), + } - ctx, err = ensureToken(ctx, []string{"changes:write"}) + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + 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"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -612,8 +619,8 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int { } delete(lf, "file") - client := AuthenticatedChangesClient(ctx) - changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) + client := AuthenticatedChangesClient(ctx, oi) + changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false) if err != nil { log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed searching for existing changes") return 1 diff --git a/cmd/invites_crud.go b/cmd/invites_crud.go index 5351bd78..3576293e 100644 --- a/cmd/invites_crud.go +++ b/cmd/invites_crud.go @@ -134,14 +134,24 @@ func InvitesRevoke(ctx context.Context) int { )) defer span.End() + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + // Authenticate - ctx, err = ensureToken(ctx, []string{"account:write"}) + ctx, err = ensureToken(ctx, oi, []string{"account:write"}) if err != nil { - log.WithContext(ctx).WithError(err).Error("failed to ensure token") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to ensure token") return 1 } - client := AuthenticatedInviteClient(ctx) + client := AuthenticatedInviteClient(ctx, oi) // Create the invite _, err = client.RevokeInvite(ctx, &connect.Request[sdp.RevokeInviteRequest]{ @@ -173,14 +183,24 @@ func InvitesCreate(ctx context.Context) int { )) defer span.End() + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + // Authenticate - ctx, err = ensureToken(ctx, []string{"account:write"}) + ctx, err = ensureToken(ctx, oi, []string{"account:write"}) if err != nil { - log.WithContext(ctx).WithError(err).Error("failed to ensure token") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to ensure token") return 1 } - client := AuthenticatedInviteClient(ctx) + client := AuthenticatedInviteClient(ctx, oi) // Create the invite _, err = client.CreateInvite(ctx, &connect.Request[sdp.CreateInviteRequest]{ @@ -206,14 +226,24 @@ func InvitesList(ctx context.Context) int { )) defer span.End() + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + // Authenticate - ctx, err = ensureToken(ctx, []string{"account:read"}) + ctx, err = ensureToken(ctx, oi, []string{"account:read"}) if err != nil { log.WithError(err).Error("failed to ensure token") return 1 } - client := AuthenticatedInviteClient(ctx) + client := AuthenticatedInviteClient(ctx, oi) // List all invites resp, err := client.ListInvites(ctx, &connect.Request[sdp.ListInvitesRequest]{}) diff --git a/cmd/request.go b/cmd/request.go index 4b30517d..ed4c8209 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -10,7 +10,6 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/internal" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" "github.com/overmindtech/sdp-go/sdpws" @@ -142,9 +141,16 @@ func Request(ctx context.Context, ready chan bool) int { )) defer span.End() - lf := log.Fields{} + lf := log.Fields{ + "app": viper.GetString("app"), + } - ctx, err = ensureToken(ctx, []string{"explore:read"}) + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + 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"}) if err != nil { log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate") return 1 @@ -161,13 +167,13 @@ func Request(ctx context.Context, ready chan bool) int { edges: []*sdp.Edge{}, msgLog: []*sdp.GatewayResponse{}, } - gatewayUrl := internal.GatewayURL(viper.GetString("url")) + gatewayUrl := oi.GatewayUrl() + lf["gateway-url"] = gatewayUrl c, err := sdpws.DialBatch(ctx, gatewayUrl, NewAuthenticatedClient(ctx, otelhttp.DefaultClient), handler, ) if err != nil { - lf["gateway-url"] = gatewayUrl log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to connect to overmind API") return 1 } diff --git a/cmd/root.go b/cmd/root.go index 20641b26..e0d0b817 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "net/url" "os" "path" @@ -21,6 +22,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/uptrace/opentelemetry-go-extra/otellogrus" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "golang.org/x/oauth2" ) @@ -30,6 +32,71 @@ var logLevel string //go:embed commit.txt var cliVersion string +type OvermindInstance struct { + FrontendUrl *url.URL + ApiUrl *url.URL + NatsUrl *url.URL +} + +// GatewayUrl returns the URL for the gateway for this instance. +func (oi OvermindInstance) GatewayUrl() string { + return fmt.Sprintf("%v/api/gateway", oi.ApiUrl.String()) +} + +type instanceData struct { + Api string `json:"api_url"` + Nats string `json:"nats_url"` +} + +// NewOvermindInstance creates a new OvermindInstance from the given app URL +// with all URLs filled in, or an error. This makes a request to the frontend to +// lookup Api and Nats URLs. +func NewOvermindInstance(ctx context.Context, app string) (OvermindInstance, error) { + var instance OvermindInstance + var err error + + instance.FrontendUrl, err = url.Parse(app) + if err != nil { + return instance, fmt.Errorf("invalid --app value '%v', error: %w", app, err) + } + + // Get the instance data + instanceDataUrl := fmt.Sprintf("%v/api/public/instance-data", instance.FrontendUrl) + req, err := http.NewRequest("GET", instanceDataUrl, nil) + if err != nil { + log.WithError(err).Fatal("could not initialize instance-data fetch") + } + + req = req.WithContext(ctx) + log.WithField("instanceDataUrl", instanceDataUrl).Debug("Fetching instance-data") + res, err := otelhttp.DefaultClient.Do(req) + if err != nil { + log.WithError(err).Fatal("could not fetch instance-data") + } + + if res.StatusCode != 200 { + log.WithField("status-code", res.StatusCode).Fatal("instance-data fetch returned non-200 status") + } + + defer res.Body.Close() + data := instanceData{} + err = json.NewDecoder(res.Body).Decode(&data) + if err != nil { + log.WithError(err).Fatal("could not parse instance-data") + } + + instance.ApiUrl, err = url.Parse(data.Api) + if err != nil { + return instance, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err) + } + instance.NatsUrl, err = url.Parse(data.Nats) + if err != nil { + return instance, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err) + } + + return instance, nil +} + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "overmind", @@ -166,14 +233,14 @@ func tokenHasAllScopes(token string, requiredScopes []string) (bool, error) { } // Gets a token using an API key -func getAPIKeyToken(ctx context.Context, apiKey string) (string, error) { +func getAPIKeyToken(ctx context.Context, oi OvermindInstance, apiKey string) (string, error) { log.WithContext(ctx).Debug("using provided token for authentication") var accessToken string if strings.HasPrefix(apiKey, "ovm_api_") { // exchange api token for JWT - client := UnauthenticatedApiKeyClient(ctx) + client := UnauthenticatedApiKeyClient(ctx, oi) resp, err := client.ExchangeKeyForToken(ctx, &connect.Request[sdp.ExchangeKeyForTokenRequest]{ Msg: &sdp.ExchangeKeyForTokenRequest{ ApiKey: apiKey, @@ -215,11 +282,11 @@ func getOauthToken(ctx context.Context, requiredScopes []string) (string, error) // keep replacing it // Check to see if the URL is secure - appurl := viper.GetString("url") + appurl := viper.GetString("app") parsed, err := url.Parse(appurl) if err != nil { - log.WithContext(ctx).WithError(err).Error("Failed to parse --url") - return "", fmt.Errorf("error parsing --url: %w", err) + log.WithContext(ctx).WithError(err).Error("Failed to parse --app") + return "", fmt.Errorf("error parsing --app: %w", err) } if !(parsed.Scheme == "wss" || parsed.Scheme == "https" || parsed.Hostname() == "localhost") { @@ -284,13 +351,13 @@ func getOauthToken(ctx context.Context, requiredScopes []string) (string, error) } // ensureToken -func ensureToken(ctx context.Context, requiredScopes []string) (context.Context, error) { +func ensureToken(ctx context.Context, oi OvermindInstance, requiredScopes []string) (context.Context, error) { var accessToken string var err error // get a token from the api key if present if apiKey := viper.GetString("api-key"); apiKey != "" { - accessToken, err = getAPIKeyToken(ctx, apiKey) + accessToken, err = getAPIKeyToken(ctx, oi, apiKey) } else { accessToken, err = getOauthToken(ctx, requiredScopes) } @@ -352,7 +419,7 @@ 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, expectedStatus sdp.ChangeStatus, errNotFound bool) (uuid.UUID, error) { +func getChangeUuid(ctx context.Context, oi OvermindInstance, expectedStatus sdp.ChangeStatus, errNotFound bool) (uuid.UUID, error) { var changeUuid uuid.UUID var err error @@ -381,7 +448,7 @@ func getChangeUuid(ctx context.Context, expectedStatus sdp.ChangeStatus, errNotF } // Finally look through all open changes to find one with a matching ticket link - client := AuthenticatedChangesClient(ctx) + client := AuthenticatedChangesClient(ctx, oi) changesList, err := client.ListChangesByStatus(ctx, &connect.Request[sdp.ListChangesByStatusRequest]{ Msg: &sdp.ListChangesByStatusRequest{ @@ -436,7 +503,7 @@ func addChangeUuidFlags(cmd *cobra.Command) { // Adds common flags to API commands e.g. timeout func addAPIFlags(cmd *cobra.Command) { cmd.PersistentFlags().String("timeout", "10m", "How long to wait for responses") - cmd.PersistentFlags().String("url", "https://api.prod.overmind.tech", "The overmind API endpoint") + cmd.PersistentFlags().String("app", "https://app.overmind.tech", "The overmind instance to connect to.") } func init() { diff --git a/cmd/snapshots_get_snapshot.go b/cmd/snapshots_get_snapshot.go index d22c6877..36821d20 100644 --- a/cmd/snapshots_get_snapshot.go +++ b/cmd/snapshots_get_snapshot.go @@ -71,11 +71,19 @@ func GetSnapshot(ctx context.Context, ready chan bool) int { )) defer span.End() - ctx, err = ensureToken(ctx, []string{"changes:read"}) + lf := log.Fields{ + "app": viper.GetString("app"), + } + + oi, err := NewOvermindInstance(ctx, viper.GetString("app")) + if err != nil { + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get instance data from app") + return 1 + } + + ctx, err = ensureToken(ctx, oi, []string{"changes:read"}) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to authenticate") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate") return 1 } @@ -83,16 +91,14 @@ func GetSnapshot(ctx context.Context, ready chan bool) int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - client := AuthenticatedSnapshotsClient(ctx) + client := AuthenticatedSnapshotsClient(ctx, oi) response, err := client.GetSnapshot(ctx, &connect.Request[sdp.GetSnapshotRequest]{ Msg: &sdp.GetSnapshotRequest{ UUID: snapshotUuid[:], }, }) if err != nil { - log.WithContext(ctx).WithError(err).WithFields(log.Fields{ - "url": viper.GetString("url"), - }).Error("failed to get snapshot") + log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to get snapshot") return 1 } log.WithContext(ctx).WithFields(log.Fields{ diff --git a/internal/paths.go b/internal/paths.go deleted file mode 100644 index 7919fa0f..00000000 --- a/internal/paths.go +++ /dev/null @@ -1,10 +0,0 @@ -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) -}