Cloud migrations: create route to fetch cloud migration token (#88176)

* Cloud migration: create route to fetch cloud migration token

* implement gcomStub.ListTokens

* fix swagger for POST /cloudmigration/migration

* fix swagger for POST /cloudmigration/migration

* fix swagger for POST /cloudmigration/migration
This commit is contained in:
Bruno 2024-05-31 09:39:10 -03:00 committed by GitHub
parent b640921dfd
commit 83543c6b12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 351 additions and 1 deletions

View File

@ -1,6 +1,7 @@
package api
import (
"errors"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
@ -47,10 +48,46 @@ func (cma *CloudMigrationAPI) registerEndpoints() {
cloudMigrationRoute.Post("/migration/:uid/run", routing.Wrap(cma.RunMigration))
cloudMigrationRoute.Get("/migration/:uid/run", routing.Wrap(cma.GetMigrationRunList))
cloudMigrationRoute.Get("/migration/run/:runUID", routing.Wrap(cma.GetMigrationRun))
cloudMigrationRoute.Get("/token", routing.Wrap(cma.GetToken))
cloudMigrationRoute.Post("/token", routing.Wrap(cma.CreateToken))
}, middleware.ReqOrgAdmin)
}
// swagger:route GET /cloudmigration/token migrations getCloudMigrationToken
//
// Fetch the cloud migration token if it exists.
//
// Responses:
// 200: cloudMigrationGetTokenResponse
// 401: unauthorisedError
// 404: notFoundError
// 403: forbiddenError
// 500: internalServerError
func (cma *CloudMigrationAPI) GetToken(c *contextmodel.ReqContext) response.Response {
ctx, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.GetToken")
defer span.End()
logger := cma.log.FromContext(ctx)
token, err := cma.cloudMigrationService.GetToken(ctx)
if err != nil {
if !errors.Is(err, cloudmigration.ErrTokenNotFound) {
logger.Error("fetching cloud migration access token", "err", err.Error())
}
return response.ErrOrFallback(http.StatusInternalServerError, "fetching cloud migration access token", err)
}
return response.JSON(http.StatusOK, GetAccessTokenResponseDTO{
ID: token.ID,
DisplayName: token.DisplayName,
ExpiresAt: token.ExpiresAt,
FirstUsedAt: token.FirstUsedAt,
LastUsedAt: token.LastUsedAt,
CreatedAt: token.CreatedAt,
})
}
// swagger:route POST /cloudmigration/token migrations createCloudMigrationToken
//
// Create gcom access token.
@ -310,6 +347,13 @@ type CloudMigrationListResponse struct {
Body cloudmigration.CloudMigrationListResponse
}
// swagger:parameters createMigration
type CreateMigration struct {
// in:body
// required:true
Body cloudmigration.CloudMigrationRequest
}
// swagger:response cloudMigrationResponse
type CloudMigrationResponse struct {
// in: body
@ -322,6 +366,25 @@ type CloudMigrationRunListResponse struct {
Body cloudmigration.CloudMigrationRunList
}
// swagger:parameters getCloudMigrationToken
type GetCloudMigrationToken struct {
}
// swagger:response cloudMigrationGetTokenResponse
type CloudMigrationGetTokenResponse struct {
// in: body
Body GetAccessTokenResponseDTO
}
type GetAccessTokenResponseDTO struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
ExpiresAt string `json:"expiresAt"`
FirstUsedAt string `json:"firstUsedAt"`
LastUsedAt string `json:"lastUsedAt"`
CreatedAt string `json:"createdAt"`
}
// swagger:response cloudMigrationCreateTokenResponse
type CloudMigrationCreateTokenResponse struct {
// in: body

View File

@ -2,10 +2,16 @@ package cloudmigration
import (
"context"
"github.com/grafana/grafana/pkg/services/gcom"
)
type Service interface {
// Returns the cloud migration token if it exists.
GetToken(context.Context) (gcom.TokenView, error)
// Creates a cloud migration token.
CreateToken(context.Context) (CreateAccessTokenResponse, error)
// Sends a request to CMS to test the token.
ValidateToken(context.Context, CloudMigration) error
CreateMigration(context.Context, CloudMigrationRequest) (*CloudMigrationResponse, error)

View File

@ -102,7 +102,7 @@ func ProvideService(
s.gcomService = gcom.New(gcom.Config{ApiURL: cfg.GrafanaComAPIURL, Token: cfg.CloudMigration.GcomAPIToken})
} else {
s.cmsClient = cmsclient.NewInMemoryClient()
s.gcomService = &gcomStub{map[string]gcom.AccessPolicy{}}
s.gcomService = &gcomStub{policies: map[string]gcom.AccessPolicy{}, token: nil}
s.cfg.StackID = "12345"
}
@ -113,6 +113,47 @@ func ProvideService(
return s, nil
}
func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) {
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetToken")
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 gcom.TokenView{}, fmt.Errorf("fetching instance by id: id=%s %w", s.cfg.StackID, err)
}
logger.Info("instance found", "slug", instance.Slug)
accessPolicyName := fmt.Sprintf("%s-%s", cloudMigrationAccessPolicyNamePrefix, s.cfg.StackID)
accessTokenName := fmt.Sprintf("%s-%s", cloudMigrationTokenNamePrefix, s.cfg.StackID)
timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.ListTokensTimeout)
defer cancel()
tokens, err := s.gcomService.ListTokens(timeoutCtx, gcom.ListTokenParams{
RequestID: requestID,
Region: instance.RegionSlug,
AccessPolicyName: accessPolicyName,
TokenName: accessTokenName})
if err != nil {
return gcom.TokenView{}, fmt.Errorf("listing tokens: %w", err)
}
logger.Info("found access tokens", "num_tokens", len(tokens))
for _, token := range tokens {
if token.Name == accessTokenName {
logger.Info("found existing cloud migration token", "tokenID", token.ID, "accessPolicyID", token.AccessPolicyID)
return token, nil
}
}
logger.Info("cloud migration token not found")
return gcom.TokenView{}, cloudmigration.ErrTokenNotFound
}
func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) {
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateToken")
defer span.End()

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/gcom"
)
// NoopServiceImpl Define the Service Implementation.
@ -11,6 +12,10 @@ type NoopServiceImpl struct{}
var _ cloudmigration.Service = (*NoopServiceImpl)(nil)
func (s *NoopServiceImpl) GetToken(ctx context.Context) (gcom.TokenView, error) {
return gcom.TokenView{}, cloudmigration.ErrFeatureDisabledError
}
func (s *NoopServiceImpl) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) {
return cloudmigration.CreateAccessTokenResponse{}, cloudmigration.ErrFeatureDisabledError
}

View File

@ -10,6 +10,8 @@ import (
)
type gcomStub struct {
// The cloud migration token created by this stub.
token *gcom.TokenView
policies map[string]gcom.AccessPolicy
}
@ -49,6 +51,14 @@ func (client *gcomStub) ListAccessPolicies(ctx context.Context, params gcom.List
return items, nil
}
func (client *gcomStub) ListTokens(ctx context.Context, params gcom.ListTokenParams) ([]gcom.TokenView, error) {
if client.token == nil {
return []gcom.TokenView{}, nil
}
return []gcom.TokenView{*client.token}, nil
}
func (client *gcomStub) CreateToken(ctx context.Context, params gcom.CreateTokenParams, payload gcom.CreateTokenPayload) (gcom.Token, error) {
token := gcom.Token{
ID: fmt.Sprintf("random-token-%s", util.GenerateShortUID()),
@ -56,5 +66,11 @@ func (client *gcomStub) CreateToken(ctx context.Context, params gcom.CreateToken
AccessPolicyID: payload.AccessPolicyID,
Token: fmt.Sprintf("completely_fake_token_%s", util.GenerateShortUID()),
}
client.token = &gcom.TokenView{
ID: token.ID,
Name: token.Name,
AccessPolicyID: token.AccessPolicyID,
DisplayName: token.Name,
}
return token, nil
}

View File

@ -14,6 +14,7 @@ var (
ErrMigrationNotFound = errutil.NotFound("cloudmigrations.migrationNotFound").Errorf("Migration not found")
ErrMigrationRunNotFound = errutil.NotFound("cloudmigrations.migrationRunNotFound").Errorf("Migration run not found")
ErrMigrationNotDeleted = errutil.Internal("cloudmigrations.migrationNotDeleted").Errorf("Migration not deleted")
ErrTokenNotFound = errutil.NotFound("cloudmigrations.tokenNotFound").Errorf("Token not found")
)
// CloudMigration api dtos

View File

@ -20,6 +20,7 @@ type Service interface {
CreateAccessPolicy(ctx context.Context, params CreateAccessPolicyParams, payload CreateAccessPolicyPayload) (AccessPolicy, error)
ListAccessPolicies(ctx context.Context, params ListAccessPoliciesParams) ([]AccessPolicy, error)
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)
}
@ -73,6 +74,13 @@ type DeleteAccessPolicyParams struct {
Region string
}
type ListTokenParams struct {
RequestID string
Region string
AccessPolicyName string
TokenName string
}
type CreateTokenParams struct {
RequestID string
Region string
@ -85,6 +93,7 @@ type CreateTokenPayload struct {
ExpiresAt time.Time `json:"expiresAt"`
}
// The token returned by gcom api when a token gets created.
type Token struct {
ID string `json:"id"`
AccessPolicyID string `json:"accessPolicyId"`
@ -92,6 +101,22 @@ type Token struct {
Token string `json:"token"`
}
// The token returned by gcom api for a GET token request.
type TokenView struct {
ID string `json:"id"`
AccessPolicyID string `json:"accessPolicyId"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
ExpiresAt string `json:"expiresAt"`
FirstUsedAt string `json:"firstUsedAt"`
LastUsedAt string `json:"lastUsedAt"`
CreatedAt string `json:"createdAt"`
}
type listTokensResponse struct {
Items []TokenView `json:"items"`
}
type GcomClient struct {
log log.Logger
cfg Config
@ -281,6 +306,50 @@ func (client *GcomClient) ListAccessPolicies(ctx context.Context, params ListAcc
return responseBody.Items, nil
}
func (client *GcomClient) ListTokens(ctx context.Context, params ListTokenParams) ([]TokenView, error) {
endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens")
if err != nil {
return nil, fmt.Errorf("building gcom tokens url: %w", err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("creating http request: %w", err)
}
query := url.Values{}
query.Set("region", params.Region)
query.Set("accessPolicyName", params.AccessPolicyName)
query.Set("name", params.TokenName)
request.URL.RawQuery = query.Encode()
request.Header.Set("x-request-id", params.RequestID)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token))
response, err := client.httpClient.Do(request)
if err != nil {
return nil, fmt.Errorf("sending http request to list access tokens: %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.StatusOK {
body, _ := io.ReadAll(response.Body)
return nil, fmt.Errorf("unexpected response when fetching access tokens: code=%d body=%s", response.StatusCode, body)
}
var body listTokensResponse
if err := json.NewDecoder(response.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("unmarshaling response body: %w", err)
}
return body.Items, nil
}
func (client *GcomClient) CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) {
endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens")
if err != nil {

View File

@ -11,6 +11,7 @@ type CloudMigrationSettings struct {
CreateAccessPolicyTimeout time.Duration
FetchAccessPolicyTimeout time.Duration
DeleteAccessPolicyTimeout time.Duration
ListTokensTimeout time.Duration
CreateTokenTimeout time.Duration
TokenExpiresAfter time.Duration
@ -25,6 +26,7 @@ func (cfg *Cfg) readCloudMigrationSettings() {
cfg.CloudMigration.CreateAccessPolicyTimeout = cloudMigration.Key("create_access_policy_timeout").MustDuration(5 * time.Second)
cfg.CloudMigration.FetchAccessPolicyTimeout = cloudMigration.Key("fetch_access_policy_timeout").MustDuration(5 * time.Second)
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.TokenExpiresAfter = cloudMigration.Key("token_expires_after").MustDuration(7 * 24 * time.Hour)
cfg.CloudMigration.IsDeveloperMode = cloudMigration.Key("developer_mode").MustBool(false)

View File

@ -2322,6 +2322,16 @@
],
"summary": "Create a migration.",
"operationId": "createMigration",
"parameters": [
{
"name": "Body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/CloudMigrationRequest"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/cloudMigrationResponse"
@ -2494,6 +2504,30 @@
}
},
"/cloudmigration/token": {
"get": {
"tags": [
"migrations"
],
"summary": "Fetch the cloud migration token if it exists.",
"operationId": "getCloudMigrationToken",
"responses": {
"200": {
"$ref": "#/responses/cloudMigrationGetTokenResponse"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
},
"post": {
"tags": [
"migrations"
@ -13207,6 +13241,14 @@
}
}
},
"CloudMigrationRequest": {
"type": "object",
"properties": {
"authToken": {
"type": "string"
}
}
},
"CloudMigrationResponse": {
"type": "object",
"properties": {
@ -15075,6 +15117,29 @@
}
}
},
"GetAccessTokenResponseDTO": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"displayName": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"firstUsedAt": {
"type": "string"
},
"id": {
"type": "string"
},
"lastUsedAt": {
"type": "string"
}
}
},
"GetAnnotationTagsResponse": {
"type": "object",
"title": "GetAnnotationTagsResponse is a response struct for FindTagsResult.",
@ -22162,6 +22227,12 @@
"$ref": "#/definitions/CreateAccessTokenResponseDTO"
}
},
"cloudMigrationGetTokenResponse": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/GetAccessTokenResponseDTO"
}
},
"cloudMigrationListResponse": {
"description": "(empty)",
"schema": {

View File

@ -183,6 +183,16 @@
},
"description": "(empty)"
},
"cloudMigrationGetTokenResponse": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetAccessTokenResponseDTO"
}
}
},
"description": "(empty)"
},
"cloudMigrationListResponse": {
"content": {
"application/json": {
@ -3656,6 +3666,14 @@
},
"type": "object"
},
"CloudMigrationRequest": {
"properties": {
"authToken": {
"type": "string"
}
},
"type": "object"
},
"CloudMigrationResponse": {
"properties": {
"created": {
@ -5524,6 +5542,29 @@
},
"type": "object"
},
"GetAccessTokenResponseDTO": {
"properties": {
"createdAt": {
"type": "string"
},
"displayName": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"firstUsedAt": {
"type": "string"
},
"id": {
"type": "string"
},
"lastUsedAt": {
"type": "string"
}
},
"type": "object"
},
"GetAnnotationTagsResponse": {
"properties": {
"result": {
@ -14995,6 +15036,17 @@
},
"post": {
"operationId": "createMigration",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloudMigrationRequest"
}
}
},
"required": true,
"x-originalParamName": "Body"
},
"responses": {
"200": {
"$ref": "#/components/responses/cloudMigrationResponse"
@ -15181,6 +15233,30 @@
}
},
"/cloudmigration/token": {
"get": {
"operationId": "getCloudMigrationToken",
"responses": {
"200": {
"$ref": "#/components/responses/cloudMigrationGetTokenResponse"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"403": {
"$ref": "#/components/responses/forbiddenError"
},
"404": {
"$ref": "#/components/responses/notFoundError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Fetch the cloud migration token if it exists.",
"tags": [
"migrations"
]
},
"post": {
"operationId": "createCloudMigrationToken",
"responses": {