feat: add migrate command to chronoctl

adds a command to migrate a db from bolt to etcd and visa versa. there
is no promise that the options/functionality will not change, hence the
'beta' note. obviously there is also no guarantee that there will never
be any data loss.
after several manual tests, i do have a high level of confidence in the
functionality presented.
pull/5363/head
greg linton 2020-02-03 13:35:47 -07:00 committed by Greg
parent 749ebf76f2
commit 1a612ffa22
7 changed files with 325 additions and 2 deletions

View File

@ -917,6 +917,8 @@ type ColumnEncoding struct {
// OrganizationConfigStore is the storage and retrieval of organization Configs
type OrganizationConfigStore interface {
// All lists all org configs in the OrganizationConfigStore
All(ctx context.Context) ([]OrganizationConfig, error)
// FindOrCreate gets an existing OrganizationConfig and creates one if none exists
FindOrCreate(ctx context.Context, orgID string) (*OrganizationConfig, error)
// Put replaces the whole organization config in the OrganizationConfigStore

50
cmd/chronoctl/README.md Normal file
View File

@ -0,0 +1,50 @@
## Chronoctl
Chronoctl is a tool to interact with an instance of a chronograf's bolt database.
```
Available commands:
add-superadmin Creates a new superadmin user (bolt specific)
list-users Lists users (bolt specific)
migrate Migrate db (beta)
```
### Migrate
The `migrate` command allows you to migrate your chronograf configuration store. It is highly recommended that you make a backup of all databases involved before running a migration as there is no guarantee that there will be no data loss. When specifying an etcd endpoint, the URI must begin with `etcd://`. It is preferred that you prefix `bolt://` to an absolute path when specifying a local bolt db file, but a lone relative path is also accepted without the prefix. If there is authentication on etcd, use the standard URI format to define a username/password: `[scheme:][//[userinfo@]host][/]path`.
There is currently no cleanup for a failed migration, so keep that in mind before migrating to a db that contains other important data.
##### Usage
```
chronoctl migrate [OPTIONS]
OPTIONS
-f, --from= Full path to boltDB file or etcd (e.g. 'bolt:///path/to/chronograf-v1.db' or 'etcd://user:pass@localhost:2379 (default: chronograf-v1.db)
-t, --to= Full path to boltDB file or etcd (e.g. 'bolt:///path/to/chronograf-v1.db' or 'etcd://user:pass@localhost:2379 (default: etcd://localhost:2379)
```
##### Example
```sh
$ chronoctl migrate -f etcd://localhost:2379 -t bolt:///tmp/chronograf.db
# Performing non-idempotent db migration from "etcd://localhost:2379" to "bolt:///tmp/chronograf.db"...
# Saved 1 organizations.
# Saved 1 organization configs.
# Saved 1 dashboards.
# Saved 3 mappings.
# Saved 0 servers.
# Saved 1 sources.
# Migration successful!
$ chronoctl migrate -f ./chronograf-v1.db -t etcd://localhost:2379
# Performing non-idempotent db migration from "./chronograf-v1.db" to "etcd://localhost:2379"...
# Saved 1 organizations.
# Saved 1 organization configs.
# Saved 1 dashboards.
# Saved 3 mappings.
# Saved 0 servers.
# Saved 1 sources.
# Migration successful!
```

226
cmd/chronoctl/migrate.go Normal file
View File

@ -0,0 +1,226 @@
package main
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/kv"
"github.com/influxdata/chronograf/kv/bolt"
"github.com/influxdata/chronograf/kv/etcd"
)
func init() {
parser.AddCommand("migrate",
"Migrate db (beta)",
"The migrate command (beta) will copy one db to another",
&migrateCommand{})
}
type migrateCommand struct {
From string `short:"f" long:"from" description:"Full path to boltDB file or etcd (e.g. 'bolt:///path/to/chronograf-v1.db' or 'etcd://user:pass@localhost:2379" default:"chronograf-v1.db"`
To string `short:"t" long:"to" description:"Full path to boltDB file or etcd (e.g. 'bolt:///path/to/chronograf-v1.db' or 'etcd://user:pass@localhost:2379" default:"etcd://localhost:2379"`
}
func (m *migrateCommand) Execute(args []string) error {
fmt.Printf("Performing non-idempotent db migration from %q to %q...\n", m.From, m.To)
if m.From == m.To {
errExit(errors.New("Cannot migrate to original source"))
}
ctx := context.TODO()
datas, err := getData(ctx, m.From)
errExit(err)
errExit(saveData(ctx, m.To, datas))
fmt.Println("Migration successful!")
return nil
}
func openService(ctx context.Context, s string) (*kv.Service, error) {
u, err := url.Parse(s)
if err != nil {
return nil, err
}
var db kv.Store
switch u.Scheme {
case "bolt", "boltdb", "":
if u.Host != "" {
return nil, errors.New("ambiguous uri")
}
db, err = bolt.NewClient(ctx,
bolt.WithPath(u.Path),
)
if err != nil {
return nil, fmt.Errorf("unable to create bolt client: %s", err)
}
case "etcd":
pw, _ := u.User.Password()
db, err = etcd.NewClient(ctx,
etcd.WithEndpoints([]string{u.Host}),
etcd.WithLogin(u.User.Username(), pw),
)
if err != nil {
return nil, fmt.Errorf("unable to create etcd client: %s", err)
}
default:
return nil, fmt.Errorf("invalid uri scheme '%s'", u.Scheme)
}
return kv.NewService(ctx, db)
}
func getData(ctx context.Context, fromURI string) (*datas, error) {
from, err := openService(ctx, fromURI)
if err != nil {
return nil, err
}
defer from.Close()
cfg, err := from.ConfigStore().Get(ctx)
if err != nil {
return nil, err
}
dashboards, err := from.DashboardsStore().All(ctx)
if err != nil {
return nil, err
}
mappings, err := from.MappingsStore().All(ctx)
if err != nil {
return nil, err
}
orgCfgs, err := from.OrganizationConfigStore().All(ctx)
if err != nil {
return nil, err
}
orgs, err := from.OrganizationsStore().All(ctx)
if err != nil {
return nil, err
}
servers, err := from.ServersStore().All(ctx)
if err != nil {
return nil, err
}
srcs, err := from.SourcesStore().All(ctx)
if err != nil {
return nil, err
}
users, err := from.UsersStore().All(ctx)
if err != nil {
return nil, err
}
return &datas{
config: *cfg,
dashboards: dashboards,
mappings: mappings,
orgConfigs: orgCfgs,
organizations: orgs,
servers: servers,
sources: srcs,
users: users,
}, nil
}
func saveData(ctx context.Context, t string, datas *datas) error {
to, err := openService(ctx, t)
if err != nil {
return fmt.Errorf("failed to open service '%s': %s", t, err)
}
defer to.Close()
err = to.ConfigStore().Update(ctx, &datas.config)
if err != nil {
return err
}
for _, org := range datas.organizations {
_, err = to.OrganizationsStore().Add(ctx, &org)
if err != nil {
if err == chronograf.ErrOrganizationAlreadyExists {
err = to.OrganizationsStore().Update(ctx, &org)
if err == nil {
continue
}
}
return fmt.Errorf("failed to add to OrganizationsStore: %s", err)
}
}
fmt.Printf(" Saved %d organizations.\n", len(datas.organizations))
for _, orgCfg := range datas.orgConfigs {
err = to.OrganizationConfigStore().Put(ctx, &orgCfg)
if err != nil {
return fmt.Errorf("failed to add to OrganizationConfigStore: %s", err)
}
}
fmt.Printf(" Saved %d organization configs.\n", len(datas.orgConfigs))
for _, dash := range datas.dashboards {
_, err = to.DashboardsStore().Add(ctx, dash)
if err != nil {
return fmt.Errorf("failed to add to DashboardsStore: %s", err)
}
}
fmt.Printf(" Saved %d dashboards.\n", len(datas.dashboards))
for _, mapping := range datas.mappings {
_, err = to.MappingsStore().Add(ctx, &mapping)
if err != nil {
return fmt.Errorf("failed to add to MappingsStore: %s", err)
}
}
fmt.Printf(" Saved %d mappings.\n", len(datas.mappings))
for _, server := range datas.servers {
_, err = to.ServersStore().Add(ctx, server)
if err != nil {
return fmt.Errorf("failed to add to ServersStore: %s", err)
}
}
fmt.Printf(" Saved %d servers.\n", len(datas.servers))
for _, source := range datas.sources {
_, err = to.SourcesStore().Add(ctx, source)
if err != nil {
return fmt.Errorf("failed to add to SourcesStore: %s", err)
}
}
fmt.Printf(" Saved %d sources.\n", len(datas.sources))
return nil
}
type datas struct {
config chronograf.Config
dashboards []chronograf.Dashboard
mappings []chronograf.Mapping
orgConfigs []chronograf.OrganizationConfig
organizations []chronograf.Organization
servers []chronograf.Server
sources []chronograf.Source
users []chronograf.User
}
func errExit(err error) {
if err == nil {
return
}
fmt.Println(err.Error())
os.Exit(1)
}

View File

@ -16,6 +16,33 @@ type organizationConfigStore struct {
client *Service
}
// All returns all known organization configurations.
func (s *organizationConfigStore) All(ctx context.Context) ([]chronograf.OrganizationConfig, error) {
var orgCfgs []chronograf.OrganizationConfig
err := s.each(ctx, func(o *chronograf.OrganizationConfig) {
orgCfgs = append(orgCfgs, *o)
})
if err != nil {
return nil, err
}
return orgCfgs, nil
}
func (s *organizationConfigStore) each(ctx context.Context, fn func(*chronograf.OrganizationConfig)) error {
return s.client.kv.View(ctx, func(tx Tx) error {
return tx.Bucket(organizationConfigBucket).ForEach(func(k, v []byte) error {
var orgCfg chronograf.OrganizationConfig
if err := internal.UnmarshalOrganizationConfig(v, &orgCfg); err != nil {
return err
}
fn(&orgCfg)
return nil
})
})
}
func (s *organizationConfigStore) get(ctx context.Context, tx Tx, orgID string, c *chronograf.OrganizationConfig) error {
v, err := tx.Bucket(organizationConfigBucket).Get([]byte(orgID))
if len(v) == 0 || err != nil {

View File

@ -9,10 +9,15 @@ import (
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
type OrganizationConfigStore struct {
AllF func(ctx context.Context) ([]chronograf.OrganizationConfig, error)
FindOrCreateF func(ctx context.Context, id string) (*chronograf.OrganizationConfig, error)
PutF func(ctx context.Context, c *chronograf.OrganizationConfig) error
}
func (s *OrganizationConfigStore) All(ctx context.Context) ([]chronograf.OrganizationConfig, error) {
return s.AllF(ctx)
}
func (s *OrganizationConfigStore) FindOrCreate(ctx context.Context, id string) (*chronograf.OrganizationConfig, error) {
return s.FindOrCreateF(ctx, id)
}

View File

@ -10,12 +10,20 @@ import (
// ensure OrganizationConfigStore implements chronograf.OrganizationConfigStore
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
// OrganizationConfigStore is an empty struct for satisfying an interface and returning errors.
type OrganizationConfigStore struct{}
// All returns an error
func (s *OrganizationConfigStore) All(context.Context) ([]chronograf.OrganizationConfig, error) {
return nil, chronograf.ErrOrganizationConfigNotFound
}
// FindOrCreate returns an error
func (s *OrganizationConfigStore) FindOrCreate(context.Context, string) (*chronograf.OrganizationConfig, error) {
return nil, chronograf.ErrOrganizationConfigNotFound
}
// Put returns an error
func (s *OrganizationConfigStore) Put(context.Context, *chronograf.OrganizationConfig) error {
return fmt.Errorf("cannot replace config")
}

View File

@ -31,12 +31,17 @@ func (s *OrganizationConfigStore) FindOrCreate(ctx context.Context, orgID string
return nil, err
}
oc, err := s.store.FindOrCreate(ctx, orgID)
return s.store.FindOrCreate(ctx, orgID)
}
// All returns all organization configs from the store.
func (s *OrganizationConfigStore) All(ctx context.Context) ([]chronograf.OrganizationConfig, error) {
var err = validOrganization(ctx)
if err != nil {
return nil, err
}
return oc, nil
return s.store.All(ctx)
}
// Put the OrganizationConfig in OrganizationConfigStore.