From 33b95440471cd5c2125a40946c56a9e8f8304f6d Mon Sep 17 00:00:00 2001 From: Bruno Date: Fri, 31 May 2024 10:03:43 -0300 Subject: [PATCH] Cloud migrations: create route to delete token (#88297) * Cloud migrations: create route to delete token * gcom.DeleteToken returns ErrTokenNotFound instead of a boolean * remove unnecessary comment * make openapi3-gen && yarn run rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts * gcom stub: implement DeleteToken --- pkg/services/cloudmigration/api/api.go | 41 +++++++++++++++ pkg/services/cloudmigration/cloudmigration.go | 1 + .../cloudmigrationimpl/cloudmigration.go | 41 ++++++++++++++- .../cloudmigrationimpl/cloudmigration_noop.go | 5 ++ .../cloudmigrationimpl/gcomstub.go | 5 ++ .../cloudmigrationimpl/metric.go | 39 ++++++-------- pkg/services/gcom/gcom.go | 52 ++++++++++++++++++- pkg/setting/setting_cloud_migration.go | 2 + public/api-merged.json | 35 +++++++++++++ .../migrate-to-cloud/api/endpoints.gen.ts | 1 - public/openapi3.json | 37 +++++++++++++ 11 files changed, 231 insertions(+), 28 deletions(-) diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index 9639fbdb985..b31885a3fe1 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -50,6 +50,7 @@ func (cma *CloudMigrationAPI) registerEndpoints() { cloudMigrationRoute.Get("/migration/run/:runUID", routing.Wrap(cma.GetMigrationRun)) cloudMigrationRoute.Get("/token", routing.Wrap(cma.GetToken)) cloudMigrationRoute.Post("/token", routing.Wrap(cma.CreateToken)) + cloudMigrationRoute.Delete("/token/:uid", routing.Wrap(cma.DeleteToken)) }, middleware.ReqOrgAdmin) } @@ -112,6 +113,34 @@ func (cma *CloudMigrationAPI) CreateToken(c *contextmodel.ReqContext) response.R return response.JSON(http.StatusOK, CreateAccessTokenResponseDTO(resp)) } +// swagger:route DELETE /cloudmigration/token/{uid} migrations deleteCloudMigrationToken +// +// Deletes a cloud migration token. +// +// Responses: +// 204: cloudMigrationDeleteTokenResponse +// 401: unauthorisedError +// 403: forbiddenError +// 500: internalServerError +func (cma *CloudMigrationAPI) DeleteToken(c *contextmodel.ReqContext) response.Response { + ctx, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.DeleteToken") + defer span.End() + + logger := cma.log.FromContext(ctx) + + uid := web.Params(c.Req)[":uid"] + if err := util.ValidateUID(uid); err != nil { + return response.Error(http.StatusBadRequest, "invalid migration uid", err) + } + + if err := cma.cloudMigrationService.DeleteToken(ctx, uid); err != nil { + logger.Error("deleting cloud migration token", "err", err.Error()) + return response.ErrOrFallback(http.StatusInternalServerError, "deleting cloud migration token", err) + } + + return response.Empty(http.StatusNoContent) +} + // swagger:route GET /cloudmigration/migration migrations getMigrationList // // Get a list of all cloud migrations. @@ -394,3 +423,15 @@ type CloudMigrationCreateTokenResponse struct { type CreateAccessTokenResponseDTO struct { Token string `json:"token"` } + +// swagger:parameters deleteCloudMigrationToken +type DeleteCloudMigrationToken struct { + // UID of a cloud migration token + // + // in: path + UID string `json:"uid"` +} + +// swagger:response cloudMigrationDeleteTokenResponse +type CloudMigrationDeleteTokenResponse struct { +} diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index 63bdfbe23e7..2ef57aeff9c 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -13,6 +13,7 @@ type Service interface { CreateToken(context.Context) (CreateAccessTokenResponse, error) // Sends a request to CMS to test the token. ValidateToken(context.Context, CloudMigration) error + DeleteToken(ctx context.Context, uid string) error CreateMigration(context.Context, CloudMigrationRequest) (*CloudMigrationResponse, error) GetMigration(ctx context.Context, uid string) (*CloudMigration, error) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index f6603534a9b..0616f77d399 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "net/http" "time" @@ -25,6 +26,8 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) // Service Define the cloudmigration.Service Implementation. @@ -106,8 +109,13 @@ func ProvideService( s.cfg.StackID = "12345" } - if err := s.registerMetrics(prom, s.metrics); err != nil { - s.log.Warn("error registering prom metrics", "error", err.Error()) + if err := prom.Register(s.metrics); err != nil { + var alreadyRegisterErr prometheus.AlreadyRegisteredError + if errors.As(err, &alreadyRegisterErr) { + s.log.Warn("cloud migration metrics already registered") + } else { + return s, fmt.Errorf("registering cloud migration metrics: %w", err) + } } return s, nil @@ -278,6 +286,35 @@ func (s *Service) ValidateToken(ctx context.Context, cm cloudmigration.CloudMigr return nil } +func (s *Service) DeleteToken(ctx context.Context, tokenID string) error { + ctx, span := s.tracer.Start(ctx, "CloudMigrationService.DeleteToken", trace.WithAttributes(attribute.String("tokenID", tokenID))) + defer span.End() + logger := s.log.FromContext(ctx) + requestID := tracing.TraceIDFromContext(ctx, false) + + timeoutCtx, cancel := context.WithTimeout(ctx, s.cfg.CloudMigration.FetchInstanceTimeout) + defer cancel() + instance, err := s.gcomService.GetInstanceByID(timeoutCtx, requestID, s.cfg.StackID) + if err != nil { + return fmt.Errorf("fetching instance by id: id=%s %w", s.cfg.StackID, err) + } + logger.Info("found instance", "instanceID", instance.ID) + + timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.DeleteTokenTimeout) + defer cancel() + if err := s.gcomService.DeleteToken(timeoutCtx, gcom.DeleteTokenParams{ + RequestID: tracing.TraceIDFromContext(ctx, false), + Region: instance.RegionSlug, + TokenID: tokenID, + }); err != nil && !errors.Is(err, gcom.ErrTokenNotFound) { + return fmt.Errorf("deleting cloud migration token: tokenID=%s %w", tokenID, err) + } + logger.Info("deleted cloud migration token", "tokenID", tokenID) + s.metrics.accessTokenDeleted.With(prometheus.Labels{"slug": s.cfg.Slug}).Inc() + + return nil +} + func (s *Service) GetMigration(ctx context.Context, uid string) (*cloudmigration.CloudMigration, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetMigration") defer span.End() diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 5c6aa3ffb34..5049470a537 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -19,6 +19,11 @@ func (s *NoopServiceImpl) GetToken(ctx context.Context) (gcom.TokenView, error) func (s *NoopServiceImpl) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) { return cloudmigration.CreateAccessTokenResponse{}, cloudmigration.ErrFeatureDisabledError } + +func (s *NoopServiceImpl) DeleteToken(ctx context.Context, uid string) error { + return cloudmigration.ErrFeatureDisabledError +} + func (s *NoopServiceImpl) ValidateToken(ctx context.Context, cm cloudmigration.CloudMigration) error { return cloudmigration.ErrFeatureDisabledError } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go b/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go index e77cdfb5ac5..ca18a1d90ec 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go @@ -74,3 +74,8 @@ func (client *gcomStub) CreateToken(ctx context.Context, params gcom.CreateToken } return token, nil } + +func (client *gcomStub) DeleteToken(ctx context.Context, params gcom.DeleteTokenParams) error { + client.token = nil + return nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/metric.go b/pkg/services/cloudmigration/cloudmigrationimpl/metric.go index 61580b3f535..b7302b2110c 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/metric.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/metric.go @@ -1,29 +1,17 @@ package cloudmigrationimpl import ( - "errors" - "fmt" - "github.com/prometheus/client_golang/prometheus" ) -// type Metrics struct { const ( namespace = "grafana" subsystem = "cloudmigrations" ) -var PromMetrics = []prometheus.Collector{ - prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "datasources_migrated", - Help: "Total amount of data sources migrated", - }, []string{"pdc_converted"}), -} - type Metrics struct { accessTokenCreated *prometheus.CounterVec + accessTokenDeleted *prometheus.CounterVec } func newMetrics() *Metrics { @@ -34,18 +22,21 @@ func newMetrics() *Metrics { Name: "access_token_created", Help: "Total of access tokens created", }, []string{"slug"}), + accessTokenDeleted: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "access_token_deleted", + Help: "Total of access tokens deleted", + }, []string{"slug"}), } } -func (s *Service) registerMetrics(prom prometheus.Registerer, metrics *Metrics) error { - if err := prom.Register(metrics.accessTokenCreated); err != nil { - var alreadyRegisterErr prometheus.AlreadyRegisteredError - if errors.As(err, &alreadyRegisterErr) { - s.log.Warn("metric already registered", "metric", metrics.accessTokenCreated) - } else { - return fmt.Errorf("registering access token created metric: %w", err) - } - } - - return nil +func (metrics *Metrics) Collect(ch chan<- prometheus.Metric) { + metrics.accessTokenCreated.Collect(ch) + metrics.accessTokenDeleted.Collect(ch) +} + +func (metrics *Metrics) Describe(ch chan<- *prometheus.Desc) { + metrics.accessTokenCreated.Describe(ch) + metrics.accessTokenDeleted.Describe(ch) } diff --git a/pkg/services/gcom/gcom.go b/pkg/services/gcom/gcom.go index c72e7d11f17..c510db5c095 100644 --- a/pkg/services/gcom/gcom.go +++ b/pkg/services/gcom/gcom.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -13,7 +14,9 @@ import ( "github.com/grafana/grafana/pkg/infra/log" ) -var LogPrefix = "gcom.service" +const LogPrefix = "gcom.service" + +var ErrTokenNotFound = errors.New("gcom: token not found") type Service interface { GetInstanceByID(ctx context.Context, requestID string, instanceID string) (Instance, error) @@ -22,6 +25,7 @@ type Service interface { DeleteAccessPolicy(ctx context.Context, params DeleteAccessPolicyParams) (bool, error) ListTokens(ctx context.Context, params ListTokenParams) ([]TokenView, error) CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) + DeleteToken(ctx context.Context, params DeleteTokenParams) error } type Instance struct { @@ -101,6 +105,12 @@ type Token struct { Token string `json:"token"` } +type DeleteTokenParams struct { + RequestID string + Region string + TokenID string +} + // The token returned by gcom api for a GET token request. type TokenView struct { ID string `json:"id"` @@ -397,3 +407,43 @@ func (client *GcomClient) CreateToken(ctx context.Context, params CreateTokenPar return token, nil } + +func (client *GcomClient) DeleteToken(ctx context.Context, params DeleteTokenParams) error { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens", params.TokenID) + if err != nil { + return fmt.Errorf("building gcom tokens url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return fmt.Errorf("sending http request to delete access token: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode == http.StatusNotFound { + return fmt.Errorf("token id: %s %w", params.TokenID, ErrTokenNotFound) + } + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(response.Body) + return fmt.Errorf("unexpected response when deleting access token: code=%d body=%s", response.StatusCode, body) + } + + return nil +} diff --git a/pkg/setting/setting_cloud_migration.go b/pkg/setting/setting_cloud_migration.go index e5de08f83f0..3c68147a5ec 100644 --- a/pkg/setting/setting_cloud_migration.go +++ b/pkg/setting/setting_cloud_migration.go @@ -13,6 +13,7 @@ type CloudMigrationSettings struct { DeleteAccessPolicyTimeout time.Duration ListTokensTimeout time.Duration CreateTokenTimeout time.Duration + DeleteTokenTimeout time.Duration TokenExpiresAfter time.Duration IsDeveloperMode bool @@ -28,6 +29,7 @@ func (cfg *Cfg) readCloudMigrationSettings() { cfg.CloudMigration.DeleteAccessPolicyTimeout = cloudMigration.Key("delete_access_policy_timeout").MustDuration(5 * time.Second) cfg.CloudMigration.ListTokensTimeout = cloudMigration.Key("list_tokens_timeout").MustDuration(5 * time.Second) cfg.CloudMigration.CreateTokenTimeout = cloudMigration.Key("create_token_timeout").MustDuration(5 * time.Second) + cfg.CloudMigration.DeleteTokenTimeout = cloudMigration.Key("delete_token_timeout").MustDuration(5 * time.Second) cfg.CloudMigration.TokenExpiresAfter = cloudMigration.Key("token_expires_after").MustDuration(7 * 24 * time.Hour) cfg.CloudMigration.IsDeveloperMode = cloudMigration.Key("developer_mode").MustBool(false) } diff --git a/public/api-merged.json b/public/api-merged.json index a426e77cecd..c0d34d48a37 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2550,6 +2550,38 @@ } } }, + "/cloudmigration/token/{uid}": { + "delete": { + "tags": [ + "migrations" + ], + "summary": "Deletes a cloud migration token.", + "operationId": "deleteCloudMigrationToken", + "parameters": [ + { + "type": "string", + "description": "UID of a cloud migration token", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/cloudMigrationDeleteTokenResponse" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/dashboard/snapshots": { "get": { "tags": [ @@ -22227,6 +22259,9 @@ "$ref": "#/definitions/CreateAccessTokenResponseDTO" } }, + "cloudMigrationDeleteTokenResponse": { + "description": "(empty)" + }, "cloudMigrationGetTokenResponse": { "description": "(empty)", "schema": { diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index d05960fa5d8..a2c48cbb401 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -142,7 +142,6 @@ export type DashboardMeta = { provisioned?: boolean; provisionedExternalId?: string; publicDashboardEnabled?: boolean; - publicDashboardUid?: string; slug?: string; type?: string; updated?: string; diff --git a/public/openapi3.json b/public/openapi3.json index bb8d9217ca5..049e953ef3e 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -183,6 +183,9 @@ }, "description": "(empty)" }, + "cloudMigrationDeleteTokenResponse": { + "description": "(empty)" + }, "cloudMigrationGetTokenResponse": { "content": { "application/json": { @@ -15279,6 +15282,40 @@ ] } }, + "/cloudmigration/token/{uid}": { + "delete": { + "operationId": "deleteCloudMigrationToken", + "parameters": [ + { + "description": "UID of a cloud migration token", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/cloudMigrationDeleteTokenResponse" + }, + "401": { + "$ref": "#/components/responses/unauthorisedError" + }, + "403": { + "$ref": "#/components/responses/forbiddenError" + }, + "500": { + "$ref": "#/components/responses/internalServerError" + } + }, + "summary": "Deletes a cloud migration token.", + "tags": [ + "migrations" + ] + } + }, "/dashboard/snapshots": { "get": { "operationId": "searchDashboardSnapshots",