From a2e21eac8cc8e4ed97c3f9ea3609a968ccf20cc0 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 25 Mar 2024 12:43:28 -0300 Subject: [PATCH] Cloud migrations: create endpoint to create an access token (#84690) * fix merge conflicts * make token expiration configurable --- .github/CODEOWNERS | 1 + conf/defaults.ini | 10 + pkg/api/frontendsettings.go | 2 +- pkg/services/cloudmigration/api/api.go | 18 +- pkg/services/cloudmigration/cloudmigration.go | 2 +- .../cloudmigrationimpl/cloudmigration.go | 144 +++++++- .../cloudmigrationimpl/cloudmigration_noop.go | 5 +- .../cloudmigrationimpl/metric.go | 51 ++- pkg/services/cloudmigration/model.go | 17 +- pkg/services/gcom/gcom.go | 330 ++++++++++++++++++ pkg/setting/setting.go | 7 +- pkg/setting/setting_cloud_migration.go | 28 ++ 12 files changed, 561 insertions(+), 54 deletions(-) create mode 100644 pkg/services/gcom/gcom.go create mode 100644 pkg/setting/setting_cloud_migration.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1b8aad620bc..a2e21194353 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -612,6 +612,7 @@ playwright.config.ts @grafana/plugins-platform-frontend # Grafana Operator Experience Team /pkg/services/caching/ @grafana/grafana-operator-experience-squad /pkg/services/cloudmigration/ @grafana/grafana-operator-experience-squad +/pkg/services/gcom/ @grafana/grafana-operator-experience-squad # Feature toggles /pkg/services/featuremgmt/ @grafana/grafana-backend-services-squad diff --git a/conf/defaults.ini b/conf/defaults.ini index dc428e9ad99..6ba5fcb5941 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1821,3 +1821,13 @@ enabled = true [cloud_migration] # Set to true to enable target-side migration UI is_target = false +# Token used to send requests to grafana com +gcom_api_token = "" +# How long to wait for a request to fetch an instance to complete +fetch_instance_timeout = 5s +# How long to wait for a request to create an access policy to complete +create_access_policy_timeout = 5s +# How long to wait for a request to create to fetch an access policy to complete +fetch_access_policy_timeout = 5s +# How long to wait for a request to create to delete an access policy to complete +delete_access_policy_timeout = 5s \ No newline at end of file diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 45c8f45e9b1..5c2cf58aace 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -167,7 +167,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) secretsManagerPluginEnabled := kvstore.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) == nil trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for")) - isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigrationIsTarget + isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget frontendSettings := &dtos.FrontendSettingsDTO{ DefaultDatasource: defaultDS, diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index 650fdd776ba..bfe83105eb8 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/cloudmigration" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -17,16 +18,19 @@ type CloudMigrationAPI struct { cloudMigrationsService cloudmigration.Service routeRegister routing.RouteRegister log log.Logger + tracer tracing.Tracer } func RegisterApi( rr routing.RouteRegister, cms cloudmigration.Service, + tracer tracing.Tracer, ) *CloudMigrationAPI { api := &CloudMigrationAPI{ log: log.New("cloudmigrations.api"), routeRegister: rr, cloudMigrationsService: cms, + tracer: tracer, } api.registerEndpoints() return api @@ -43,15 +47,23 @@ func (cma *CloudMigrationAPI) registerEndpoints() { cloudMigrationRoute.Post("/migration/:id/run", routing.Wrap(cma.RunMigration)) cloudMigrationRoute.Get("/migration/:id/run", routing.Wrap(cma.GetMigrationRunList)) cloudMigrationRoute.Get("/migration/:id/run/:runID", routing.Wrap(cma.GetMigrationRun)) + cloudMigrationRoute.Post("/token", routing.Wrap(cma.CreateToken)) }, middleware.ReqGrafanaAdmin) } func (cma *CloudMigrationAPI) CreateToken(c *contextmodel.ReqContext) response.Response { - err := cma.cloudMigrationsService.CreateToken(c.Req.Context()) + ctx, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.CreateAccessToken") + defer span.End() + + logger := cma.log.FromContext(ctx) + + resp, err := cma.cloudMigrationsService.CreateToken(ctx) if err != nil { - return response.Error(http.StatusInternalServerError, "token creation error", err) + logger.Error("creating gcom access token", "err", err.Error()) + return response.Error(http.StatusInternalServerError, "creating gcom access token", err) } - return response.Success("Token created") + + return response.JSON(http.StatusOK, cloudmigration.CreateAccessTokenResponseDTO(resp)) } func (cma *CloudMigrationAPI) GetMigrationList(c *contextmodel.ReqContext) response.Response { diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index c66bbda45ce..9efa5ebc36a 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -5,7 +5,7 @@ import ( ) type Service interface { - CreateToken(context.Context) error + CreateToken(context.Context) (CreateAccessTokenResponse, error) ValidateToken(context.Context, string) error SaveEncryptedToken(context.Context, string) error // migration diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index b66667393b0..0188d160644 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -2,14 +2,20 @@ package cloudmigrationimpl import ( "context" + "encoding/base64" + "encoding/json" + "fmt" + "time" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/cloudmigration" "github.com/grafana/grafana/pkg/services/cloudmigration/api" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/gcom" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" ) @@ -18,18 +24,27 @@ import ( type Service struct { store store - log log.Logger + log *log.ConcreteLogger cfg *setting.Cfg - features featuremgmt.FeatureToggles - dsService datasources.DataSourceService + features featuremgmt.FeatureToggles + dsService datasources.DataSourceService + gcomService gcom.Service - api *api.CloudMigrationAPI - // metrics *Metrics + api *api.CloudMigrationAPI + tracer tracing.Tracer + metrics *Metrics } var LogPrefix = "cloudmigration.service" +const ( + // nolint:gosec + cloudMigrationAccessPolicyName = "grafana-cloud-migrations" + //nolint:gosec + cloudMigrationTokenName = "grafana-cloud-migrations" +) + var _ cloudmigration.Service = (*Service)(nil) // ProvideService Factory for method used by wire to inject dependencies. @@ -41,30 +56,129 @@ func ProvideService( dsService datasources.DataSourceService, routeRegister routing.RouteRegister, prom prometheus.Registerer, + tracer tracing.Tracer, ) cloudmigration.Service { if !features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) { return &NoopServiceImpl{} } s := &Service{ - store: &sqlStore{db: db}, - log: log.New(LogPrefix), - cfg: cfg, - features: features, - dsService: dsService, + store: &sqlStore{db: db}, + log: log.New(LogPrefix), + cfg: cfg, + features: features, + dsService: dsService, + gcomService: gcom.New(gcom.Config{ApiURL: cfg.GrafanaComAPIURL, Token: cfg.CloudMigration.GcomAPIToken}), + tracer: tracer, + metrics: newMetrics(), } - s.api = api.RegisterApi(routeRegister, s) + s.api = api.RegisterApi(routeRegister, s, tracer) - if err := s.registerMetrics(prom); err != nil { + if err := s.registerMetrics(prom, s.metrics); err != nil { s.log.Warn("error registering prom metrics", "error", err.Error()) } return s } -func (s *Service) CreateToken(ctx context.Context) error { - // TODO: Implement method - return nil +func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) { + ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateToken") + 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 cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("fetching instance by id: id=%s %w", s.cfg.StackID, err) + } + + timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.FetchAccessPolicyTimeout) + defer cancel() + existingAccessPolicy, err := s.findAccessPolicyByName(timeoutCtx, instance.RegionSlug, cloudMigrationAccessPolicyName) + if err != nil { + return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("fetching access policy by name: name=%s %w", cloudMigrationAccessPolicyName, err) + } + + if existingAccessPolicy != nil { + timeoutCtx, cancel := context.WithTimeout(ctx, s.cfg.CloudMigration.DeleteAccessPolicyTimeout) + defer cancel() + if _, err := s.gcomService.DeleteAccessPolicy(timeoutCtx, gcom.DeleteAccessPolicyParams{ + RequestID: requestID, + AccessPolicyID: existingAccessPolicy.ID, + Region: instance.RegionSlug, + }); err != nil { + return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("deleting access policy: id=%s region=%s %w", existingAccessPolicy.ID, instance.RegionSlug, err) + } + logger.Info("deleted access policy", existingAccessPolicy.ID, "name", existingAccessPolicy.Name) + } + + timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.CreateAccessPolicyTimeout) + defer cancel() + accessPolicy, err := s.gcomService.CreateAccessPolicy(timeoutCtx, + gcom.CreateAccessPolicyParams{ + RequestID: requestID, + Region: instance.RegionSlug, + }, + gcom.CreateAccessPolicyPayload{ + Name: cloudMigrationAccessPolicyName, + DisplayName: cloudMigrationAccessPolicyName, + Realms: []gcom.Realm{{Type: "stack", Identifier: s.cfg.StackID, LabelPolicies: []gcom.LabelPolicy{}}}, + Scopes: []string{"cloud-migrations:read", "cloud-migrations:write"}, + }) + if err != nil { + return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("creating access policy: %w", err) + } + logger.Info("created access policy", "id", accessPolicy.ID, "name", accessPolicy.Name) + + timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.CreateTokenTimeout) + defer cancel() + token, err := s.gcomService.CreateToken(timeoutCtx, + gcom.CreateTokenParams{RequestID: requestID, Region: instance.RegionSlug}, + gcom.CreateTokenPayload{ + AccessPolicyID: accessPolicy.ID, + DisplayName: cloudMigrationTokenName, + Name: cloudMigrationTokenName, + ExpiresAt: time.Now().Add(s.cfg.CloudMigration.TokenExpiresAfter), + }) + if err != nil { + return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("creating access token: %w", err) + } + logger.Info("created access token", "id", token.ID, "name", token.Name) + s.metrics.accessTokenCreated.With(prometheus.Labels{"slug": s.cfg.Slug}).Inc() + + bytes, err := json.Marshal(map[string]string{ + "token": token.Token, + "region": instance.ClusterSlug, + }) + if err != nil { + return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("encoding token: %w", err) + } + + return cloudmigration.CreateAccessTokenResponse{Token: base64.StdEncoding.EncodeToString(bytes)}, nil +} + +func (s *Service) findAccessPolicyByName(ctx context.Context, regionSlug, accessPolicyName string) (*gcom.AccessPolicy, error) { + ctx, span := s.tracer.Start(ctx, "CloudMigrationService.findAccessPolicyByName") + defer span.End() + + accessPolicies, err := s.gcomService.ListAccessPolicies(ctx, gcom.ListAccessPoliciesParams{ + RequestID: tracing.TraceIDFromContext(ctx, false), + Region: regionSlug, + Name: accessPolicyName, + }) + if err != nil { + return nil, fmt.Errorf("listing access policies: name=%s region=%s :%w", accessPolicyName, regionSlug, err) + } + + for _, accessPolicy := range accessPolicies { + if accessPolicy.Name == accessPolicyName { + return &accessPolicy, nil + } + } + + return nil, nil } func (s *Service) ValidateToken(ctx context.Context, token string) error { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 72c3c101327..a1ec2c174f7 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -15,10 +15,9 @@ func (s *NoopServiceImpl) MigrateDatasources(ctx context.Context, request *cloud return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) CreateToken(ctx context.Context) error { - return cloudmigration.ErrFeatureDisabledError +func (s *NoopServiceImpl) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) { + return cloudmigration.CreateAccessTokenResponse{}, cloudmigration.ErrFeatureDisabledError } - func (s *NoopServiceImpl) ValidateToken(ctx context.Context, token string) error { return cloudmigration.ErrFeatureDisabledError } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/metric.go b/pkg/services/cloudmigration/cloudmigrationimpl/metric.go index 0c33a7adfb9..61580b3f535 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/metric.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/metric.go @@ -2,25 +2,50 @@ package cloudmigrationimpl import ( "errors" + "fmt" - "github.com/grafana/grafana/pkg/services/cloudmigration" "github.com/prometheus/client_golang/prometheus" ) // type Metrics struct { -// log log.Logger -// } - -func (s *Service) registerMetrics(prom prometheus.Registerer) error { - for _, m := range cloudmigration.PromMetrics { - if err := prom.Register(m); err != nil { - var alreadyRegisterErr prometheus.AlreadyRegisteredError - if errors.As(err, &alreadyRegisterErr) { - s.log.Warn("metric already registered", "metric", m) - continue - } - return err +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 +} + +func newMetrics() *Metrics { + return &Metrics{ + accessTokenCreated: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "access_token_created", + Help: "Total of access tokens created", + }, []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 } diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index eeb76225d34..4a17c543ece 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -4,7 +4,6 @@ import ( "time" "github.com/grafana/grafana/pkg/util/errutil" - "github.com/prometheus/client_golang/prometheus" ) var ( @@ -77,16 +76,10 @@ type MigrateDatasourcesResponseDTO struct { DatasourcesMigrated int `json:"datasourcesMigrated"` } -const ( - namespace = "grafana" - subsystem = "cloudmigrations" -) +type CreateAccessTokenResponse struct { + Token string +} -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 CreateAccessTokenResponseDTO struct { + Token string `json:"token"` } diff --git a/pkg/services/gcom/gcom.go b/pkg/services/gcom/gcom.go new file mode 100644 index 00000000000..dbcbeb1bf65 --- /dev/null +++ b/pkg/services/gcom/gcom.go @@ -0,0 +1,330 @@ +package gcom + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/grafana/grafana/pkg/infra/log" +) + +var LogPrefix = "gcom.service" + +type Service interface { + GetInstanceByID(ctx context.Context, requestID string, instanceID string) (Instance, error) + 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) + CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) +} + +type Instance struct { + ID int `json:"id"` + Slug string `json:"slug"` + RegionSlug string `json:"regionSlug"` + ClusterSlug string `json:"clusterSlug"` +} + +type CreateAccessPolicyParams struct { + RequestID string + Region string +} + +type CreateAccessPolicyPayload struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Realms []Realm `json:"realms"` + Scopes []string `json:"scopes"` +} + +type Realm struct { + Identifier string `json:"identifier"` + LabelPolicies []LabelPolicy `json:"labelPolicies"` + Type string `json:"type"` +} + +type LabelPolicy struct { + Selector string `json:"selector"` +} + +type AccessPolicy struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type ListAccessPoliciesParams struct { + RequestID string + Region string + Name string +} + +type listAccessPoliciesResponse struct { + Items []AccessPolicy `json:"items"` +} + +type DeleteAccessPolicyParams struct { + RequestID string + AccessPolicyID string + Region string +} + +type CreateTokenParams struct { + RequestID string + Region string +} + +type CreateTokenPayload struct { + AccessPolicyID string `json:"accessPolicyId"` + DisplayName string `json:"displayName"` + Name string `json:"name"` + ExpiresAt time.Time `json:"expiresAt"` +} + +type Token struct { + ID string `json:"id"` + AccessPolicyID string `json:"accessPolicyId"` + Name string `json:"name"` + Token string `json:"token"` +} + +type GcomClient struct { + log log.Logger + cfg Config + httpClient *http.Client +} + +type Config struct { + ApiURL string + Token string +} + +func New(cfg Config) Service { + return &GcomClient{ + log: log.New(LogPrefix), + cfg: cfg, + httpClient: &http.Client{}, + } +} + +func (client *GcomClient) GetInstanceByID(ctx context.Context, requestID string, instanceID string) (Instance, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/instances/", instanceID) + if err != nil { + return Instance{}, fmt.Errorf("building gcom instance url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return Instance{}, fmt.Errorf("creating http request: %w", err) + } + + request.Header.Set("x-request-id", 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 Instance{}, fmt.Errorf("sending http request to create fetch instance by id: %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 Instance{}, fmt.Errorf("unexpected response when fetching instance by id: code=%d body=%s", response.StatusCode, body) + } + + var instance Instance + if err := json.NewDecoder(response.Body).Decode(&instance); err != nil { + return instance, fmt.Errorf("unmarshaling response body: %w", err) + } + + return instance, nil +} + +func (client *GcomClient) CreateAccessPolicy(ctx context.Context, params CreateAccessPolicyParams, payload CreateAccessPolicyPayload) (AccessPolicy, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") + if err != nil { + return AccessPolicy{}, fmt.Errorf("building gcom access policy url: %w", err) + } + + body, err := json.Marshal(&payload) + if err != nil { + return AccessPolicy{}, fmt.Errorf("marshaling request body: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return AccessPolicy{}, 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("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 AccessPolicy{}, fmt.Errorf("sending http request to create access policy: %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 AccessPolicy{}, fmt.Errorf("unexpected response when creating access policy: code=%d body=%s", response.StatusCode, body) + } + + var accessPolicy AccessPolicy + if err := json.NewDecoder(response.Body).Decode(&accessPolicy); err != nil { + return accessPolicy, fmt.Errorf("unmarshaling response body: %w", err) + } + + return accessPolicy, nil +} + +func (client *GcomClient) DeleteAccessPolicy(ctx context.Context, params DeleteAccessPolicyParams) (bool, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies/", params.AccessPolicyID) + if err != nil { + return false, fmt.Errorf("building gcom access policy url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return false, 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 false, fmt.Errorf("sending http request to create access policy: %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 false, nil + } + + if response.StatusCode == http.StatusOK || response.StatusCode == http.StatusNoContent { + return true, nil + } + + body, _ := io.ReadAll(response.Body) + return false, fmt.Errorf("unexpected response when deleting access policy: code=%d body=%s", response.StatusCode, body) +} + +func (client *GcomClient) ListAccessPolicies(ctx context.Context, params ListAccessPoliciesParams) ([]AccessPolicy, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") + if err != nil { + return nil, fmt.Errorf("building gcom access policy 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("name", params.Name) + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Accept", "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 create access policy: %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 listing access policies: code=%d body=%s", response.StatusCode, body) + } + + var responseBody listAccessPoliciesResponse + if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { + return responseBody.Items, fmt.Errorf("unmarshaling response body: %w", err) + } + + return responseBody.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 { + return Token{}, fmt.Errorf("building gcom tokens url: %w", err) + } + + body, err := json.Marshal(&payload) + if err != nil { + return Token{}, fmt.Errorf("marshaling request body: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return Token{}, 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("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 Token{}, fmt.Errorf("sending http request to create 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.StatusOK { + body, _ := io.ReadAll(response.Body) + return Token{}, fmt.Errorf("unexpected response when creating access token: code=%d body=%s", response.StatusCode, body) + } + + var token Token + if err := json.NewDecoder(response.Body).Decode(&token); err != nil { + return token, fmt.Errorf("unmarshaling response body: %w", err) + } + + return token, nil +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index aba1c19e281..a76604b1975 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -495,7 +495,7 @@ type Cfg struct { PublicDashboardsEnabled bool // Cloud Migration - CloudMigrationIsTarget bool + CloudMigration CloudMigrationSettings // Feature Management Settings FeatureManagement FeatureMgmtSettings @@ -1996,8 +1996,3 @@ func (cfg *Cfg) readPublicDashboardsSettings() { publicDashboards := cfg.Raw.Section("public_dashboards") cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true) } - -func (cfg *Cfg) readCloudMigrationSettings() { - cloudMigration := cfg.Raw.Section("cloud_migration") - cfg.CloudMigrationIsTarget = cloudMigration.Key("is_target").MustBool(false) -} diff --git a/pkg/setting/setting_cloud_migration.go b/pkg/setting/setting_cloud_migration.go new file mode 100644 index 00000000000..7519ae535b7 --- /dev/null +++ b/pkg/setting/setting_cloud_migration.go @@ -0,0 +1,28 @@ +package setting + +import ( + "time" +) + +type CloudMigrationSettings struct { + IsTarget bool + GcomAPIToken string + FetchInstanceTimeout time.Duration + CreateAccessPolicyTimeout time.Duration + FetchAccessPolicyTimeout time.Duration + DeleteAccessPolicyTimeout time.Duration + CreateTokenTimeout time.Duration + TokenExpiresAfter time.Duration +} + +func (cfg *Cfg) readCloudMigrationSettings() { + cloudMigration := cfg.Raw.Section("cloud_migration") + cfg.CloudMigration.IsTarget = cloudMigration.Key("is_target").MustBool(false) + cfg.CloudMigration.GcomAPIToken = cloudMigration.Key("gcom_api_token").MustString("") + cfg.CloudMigration.FetchInstanceTimeout = cloudMigration.Key("fetch_instance_timeout").MustDuration(5 * time.Second) + 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.CreateTokenTimeout = cloudMigration.Key("create_token_timeout").MustDuration(5 * time.Second) + cfg.CloudMigration.TokenExpiresAfter = cloudMigration.Key("token_expires_after").MustDuration(7 * 24 * time.Hour) +}