mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
3e872bb77e
commit
33b9544047
@ -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 {
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -142,7 +142,6 @@ export type DashboardMeta = {
|
||||
provisioned?: boolean;
|
||||
provisionedExternalId?: string;
|
||||
publicDashboardEnabled?: boolean;
|
||||
publicDashboardUid?: string;
|
||||
slug?: string;
|
||||
type?: string;
|
||||
updated?: string;
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user