Skip to content

Commit

Permalink
Add support for Managed Grafana v10 (#28)
Browse files Browse the repository at this point in the history
* Introduce support for service account tokens

* Update README

* Update tests

* Bump binary version

* Fix existing tests

* Update SDK for v10 support

* Go mod tidy
  • Loading branch information
bonclay7 authored May 15, 2024
1 parent aa24334 commit a7f291a
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 94 deletions.
57 changes: 48 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
[![Build](https://github.com/aws-observability/amazon-managed-grafana-migrator/actions/workflows/go.yml/badge.svg)](https://github.com/aws-observability/amazon-managed-grafana-migrator/actions/workflows/go.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/aws-observability/amazon-managed-grafana-migrator)](https://goreportcard.com/report/github.com/aws-observability/amazon-managed-grafana-migrator)

🚨 Jul-25: Alerts migration are currently disabled with v0.1.9. See [#19](https://github.com/aws-observability/amazon-managed-grafana-migrator/issues/19)
🎉 May-15-24: v0.2.0 supports Grafana Service Accounts for v9 and v10 workspaces.
See [Amazon Managed Grafana announces support for Grafana version 10.4]()

🎉 Jul-19: [Amazon Grafana supports now in-place update from v8.4 to v9.4](https://aws.amazon.com/about-aws/whats-new/2023/07/amazon-managed-grafana-in-place-update-version-9-4/)
🚨 Jul-25-23: Alerts migration are currently disabled with v0.1.9. See [#19](https://github.com/aws-observability/amazon-managed-grafana-migrator/issues/19)

🎉 Jul-19-23: [Amazon Grafana supports now in-place update from v8.4 to v9.4](https://aws.amazon.com/about-aws/whats-new/2023/07/amazon-managed-grafana-in-place-update-version-9-4/)

Amazon Managed Grafana Migrator is a CLI migration utility to migrate Grafana
content (data sources, dashboards, folders and alert rules) to Amazon Managed
Grafana. It supports the following migration scenarios:

- Migrating from and to Amazon Managed Grafana Workspace (eg. Moving to v9.4), although consider using the native functionality in the AWS Console
- Migrating from and to Amazon Managed Grafana Workspace (eg. Moving to v10.4), although consider using the native functionality in the AWS Console, after testing
- Migrating from a Grafana server to an Amazon Managed Grafana Workspace

<img src="https://user-images.githubusercontent.com/10175027/235176809-9b71af1a-79a9-416a-b26e-ccdf725779d7.gif" width="80%" height="80%"/>


:warning: Alerting rules migration are only supported in a migration to Amazon
Managed Grafana v9.4. (migrating alerts to v8.x is not supported)
Amazon Managed Grafana v10.4 workspaces will require to
provide an `ADMIN` level [Grafana Service Account]() with the
`--src-service-account-id` or `--src-service-account-id` flags.

## Installation

Expand Down Expand Up @@ -52,6 +55,30 @@ amazon-managed-grafana-migrator -v
amazon-managed-grafana-migrator discover --region eu-west-1
```

### Migrating to Amazon Managed Grafana v10

v9 and v10 introduced Grafana Service Accounts which will be required by the
migrator, especially for v10. Note that Service Accounts are billed as active
users

1. Creating a Service Account

```console
aws grafana create-workspace-service-account --workspace-id g-abcdef5678 \
--grafana-role ADMIN \
--name <SA_NAME_HERE>
```

2. Running the migration

```console
amazon-managed-grafana-migrator migrate \
--src-url https://grafana.example.com/
--src-api-key API_KEY_HERE
--dst g-abcdef5678.grafana-workspace.us-west-2.amazonaws.com
--dst-service-account-id SERVICE_ACCOUNT_ID_HERE
```

### Migrating between Workspaces

```console
Expand All @@ -60,7 +87,17 @@ amazon-managed-grafana-migrator migrate \
--dst g-abcdef5678.grafana-workspace.us-west-2.amazonaws.com
```

### Migrating to Amazon Managed Grafana
Or for v9+ workspaces:

```console
amazon-managed-grafana-migrator migrate \
--src g-abcdef1234.grafana-workspace.eu-central-1.amazonaws.com \
--src-service-account-id SERVICE_ACCOUNT_ID_HERE
--dst g-abcdef5678.grafana-workspace.us-west-2.amazonaws.com
--dst-service-account-id SERVICE_ACCOUNT_ID_HERE
```

### Migrating to Amazon Managed Grafana v8/v9

```console
amazon-managed-grafana-migrator migrate \
Expand Down Expand Up @@ -91,9 +128,11 @@ start. Below are the minimum permissions required by the tool:
{
"Effect": "Allow",
"Action": [
"grafana:DeleteWorkspaceApiKey",
"grafana:DescribeWorkspace",
"grafana:CreateWorkspaceApiKey"
"grafana:CreateWorkspaceApiKey",
"grafana:DeleteWorkspaceApiKey",
"grafana:CreateWorkspaceServiceAccountToken",
"grafana:DeleteWorkspaceServiceAccountToken"
],
"Resource": "arn:aws:grafana:*:<ACCOUNT_ID>:/workspaces/<WORKSPACE_ID>"
},
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/aws/aws-sdk-go v1.51.30
github.com/fatih/color v1.16.0
github.com/aws/aws-sdk-go v1.53.3
github.com/fatih/color v1.17.0
github.com/golang/mock v1.6.0
github.com/grafana/grafana-api-golang-client v0.27.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
github.com/aws/aws-sdk-go v1.51.30 h1:RVFkjn9P0JMwnuZCVH0TlV5k9zepHzlbc4943eZMhGw=
github.com/aws/aws-sdk-go v1.51.30/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.53.3 h1:xv0iGCCLdf6ZtlLPMCBjm+tU9UBLP5hXnSqnbKFYmto=
github.com/aws/aws-sdk-go v1.53.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
Expand Down Expand Up @@ -51,8 +51,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
9 changes: 1 addition & 8 deletions internal/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type App struct {
// const minAlertingMigrationVersion = 9.4

// Run orchestrates the migration of grafana contents
func (a *App) Run(srcCustomGrafanaClient CustomGrafanaClient) error {
func (a *App) Run() error {
log.Info()
migratedDs, err := a.migrateDataSources()
if err != nil {
Expand All @@ -54,12 +54,5 @@ func (a *App) Run(srcCustomGrafanaClient CustomGrafanaClient) error {
log.Info()

log.Info("Skipping alert rules migration")
/*
alertsMigrated, err := a.migrateAlertRules(fx, srcCustomGrafanaClient)
if err != nil {
return err
}
log.Success("Migrated ", alertsMigrated, " alerts")
*/
return nil
}
2 changes: 1 addition & 1 deletion internal/pkg/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func TestApp_Run(t *testing.T) {
Dst: mockDst,
}

err := app.Run(CustomGrafanaClient{Client: mockcustomAPI})
err := app.Run()
if tc.expectedError != nil {
require.EqualError(t, err, tc.expectedError.Error())
} else {
Expand Down
81 changes: 53 additions & 28 deletions internal/pkg/app/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,45 @@ import (
gapi "github.com/grafana/grafana-api-golang-client"
)

const (
AMG_V10 = "10.4"
AMG_V9 = "9.4"
AMG_V8 = "8.4"
)

// GrafanaInput holds the infos about the grafana server from the CLI
type GrafanaInput struct {
URL string
WorkspaceID string
APIKey string
Region string
IsAMG bool
IsGamma bool
URL string
WorkspaceID string
APIKey string
Region string
ServiceAccountID string
WorkspaceVersion string
IsAMG bool
IsGamma bool
}

// GrafanaHTTPClient contains the grafana client and AWS API key
type GrafanaHTTPClient struct {
Client *gapi.Client
Key aws.AMGApiKey
Auth aws.GrafanaAuth
Input *GrafanaInput
}

// NewGrafanaInput validate input from command line to return a GrafanaInput object
func NewGrafanaInput(wkspEndpoint, url, apiKey string) (GrafanaInput, error) {

func NewGrafanaInput(wkspEndpoint, url, serviceAccountID, apiKey string) (GrafanaInput, error) {
if wkspEndpoint != "" {
sx := strings.Split(wkspEndpoint, ".")
if len(sx) != 5 {
return GrafanaInput{}, fmt.Errorf("invalid input: workspace should be its DNS endpoint")
}
return GrafanaInput{
WorkspaceID: sx[0],
Region: sx[2],
URL: wkspEndpoint,
IsAMG: true,
IsGamma: strings.Contains(sx[1], "gamma"),
WorkspaceID: sx[0],
Region: sx[2],
URL: wkspEndpoint,
ServiceAccountID: serviceAccountID,
IsAMG: true,
IsGamma: strings.Contains(sx[1], "gamma"),
}, nil
} else if url != "" && apiKey != "" {
return GrafanaInput{
Expand All @@ -54,8 +62,9 @@ func NewGrafanaInput(wkspEndpoint, url, apiKey string) (GrafanaInput, error) {
return GrafanaInput{}, errors.New("invalid input")
}

// getAPIKey create api keys only when provided with a managed grafana ID
func (input *GrafanaInput) getAPIKey(awsgrafanacli *aws.AMG) (aws.AMGApiKey, error) {
// getGrafanaAuthToken create Grafana api keys only when provided with a managed grafana ID
// if service account is provided, it will create a service account token
func (input *GrafanaInput) getGrafanaAuthToken(awsgrafanacli *aws.AMG) (aws.GrafanaAuth, error) {

if !input.IsAMG {
log.InfoLight("Skipping API key creation for ", input.URL)
Expand All @@ -64,45 +73,61 @@ func (input *GrafanaInput) getAPIKey(awsgrafanacli *aws.AMG) (aws.AMGApiKey, err
}, nil
}

key, err := awsgrafanacli.CreateWorkspaceApiKey(input.WorkspaceID)
if err != nil {
return key, err
wksp, err := awsgrafanacli.DescribeWorkspace(input.WorkspaceID)
if err == nil {
input.WorkspaceVersion = wksp.Version
}

// forcing V10 to use service account token
if input.WorkspaceVersion == AMG_V10 && input.ServiceAccountID == "" {
return nil, errors.New("input error: service account ID is required for AMG v10, run migrate -h for help")
}
return key, nil

// creating service account token if service account is provided
if input.ServiceAccountID != "" {
return awsgrafanacli.CreateServiceAccountToken(input.WorkspaceID, input.ServiceAccountID)
}

// creating temporary API key if no service account is provided
return awsgrafanacli.CreateWorkspaceApiKey(input.WorkspaceID)
}

// CreateGrafanaAPIClient create a grafana HTTP API client from the input
func (input *GrafanaInput) CreateGrafanaAPIClient(awsgrafanacli *aws.AMG) (*GrafanaHTTPClient, error) {
var url string

if input.IsAMG {
// could be replaced by describe workspace
url = fmt.Sprintf("https://%s", input.URL)
} else {
url = input.URL
}

// get API keys
apiKey, err := input.getAPIKey(awsgrafanacli)
// get final auth key or token
apiKey, err := input.getGrafanaAuthToken(awsgrafanacli)
if err != nil {
return nil, err
}

client, err := gapi.New(url, gapi.Config{APIKey: apiKey.APIKey})
client, err := gapi.New(url, gapi.Config{APIKey: apiKey.GetAuth()})
if err != nil {
return nil, err
}
return &GrafanaHTTPClient{
Client: client,
Key: apiKey,
Auth: apiKey,
Input: input,
}, nil
}

// DeleteAPIKeys delete the temporary API key from the AWS grafana workspace
func (input *GrafanaInput) DeleteAPIKeys(awsgrafanacli *aws.AMG, apiKey aws.AMGApiKey) error {
// DeleteGrafanaAuth delete the temporary API key from the AWS grafana workspace
func (input *GrafanaInput) DeleteGrafanaAuth(awsgrafanacli *aws.AMG, auth aws.GrafanaAuth) error {
if !input.IsAMG {
return nil
}
return awsgrafanacli.DeleteWorkspaceApiKey(apiKey)

if input.ServiceAccountID != "" {
return awsgrafanacli.DeleteServiceAccountToken(auth.(aws.AMGServiceAccountToken))
}

return awsgrafanacli.DeleteWorkspaceApiKey(auth.(aws.AMGApiKey))
}
Loading

0 comments on commit a7f291a

Please sign in to comment.