Skip to content

Commit 7bca0c2

Browse files
committed
Add tool to migrate data from badger to mysql or postgresql
1 parent 90bac46 commit 7bca0c2

File tree

2 files changed

+330
-0
lines changed

2 files changed

+330
-0
lines changed

scripts/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22

33
Please note that `install-step-ra.sh` is referenced on the `files.smallstep.com` S3 website bucket as a redirect to `raw.githubusercontent.com`. If you move it, please update the S3 redirect.
44

5+
## badger-migration
6+
7+
badger-migration is a tool that allows to migrate data from a BadgerDB (v1 or
8+
v2) to MySQL or PostgreSQL.

scripts/badger-migration/main.go

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"errors"
7+
"flag"
8+
"fmt"
9+
"log"
10+
"os"
11+
"path/filepath"
12+
13+
badgerv1 "github.com/dgraph-io/badger"
14+
badgerv2 "github.com/dgraph-io/badger/v2"
15+
16+
"github.com/smallstep/nosql"
17+
)
18+
19+
var (
20+
authorityTables = []string{
21+
"x509_certs",
22+
"x509_certs_data",
23+
"revoked_x509_certs",
24+
"x509_crl",
25+
"revoked_ssh_certs",
26+
"used_ott",
27+
"ssh_certs",
28+
"ssh_hosts",
29+
"ssh_users",
30+
"ssh_host_principals",
31+
}
32+
acmeTables = []string{
33+
"acme_accounts",
34+
"acme_keyID_accountID_index",
35+
"acme_authzs",
36+
"acme_challenges",
37+
"nonces",
38+
"acme_orders",
39+
"acme_account_orders_index",
40+
"acme_certs",
41+
"acme_serial_certs_index",
42+
"acme_external_account_keys",
43+
"acme_external_account_keyID_reference_index",
44+
"acme_external_account_keyID_provisionerID_index",
45+
}
46+
)
47+
48+
func usage(fs *flag.FlagSet) {
49+
name := filepath.Base(os.Args[0])
50+
fmt.Fprintf(os.Stderr, "%s is a tool to migrate Badger databases to MySQL or PostgreSQL.\n", name)
51+
fmt.Fprintln(os.Stderr, "\nUsage:")
52+
fmt.Fprintf(os.Stderr, " %s [-v1|-v2] -dir=<path> [-value-dir=<path>] -type=type -database=<source>\n", name)
53+
fmt.Fprintln(os.Stderr, "\nExamples:")
54+
fmt.Fprintf(os.Stderr, " %s -v1 -dir /var/lib/step-ca/db -type=mysql -database \"user@unix/step_ca\"\n", name)
55+
fmt.Fprintf(os.Stderr, " %s -v2 -dir /var/lib/step-ca/db -type=mysql -database \"user:password@tcp(localhost:3306)/step_ca\"\n", name)
56+
fmt.Fprintf(os.Stderr, " %s -v2 -dir /var/lib/step-ca/db -type=postgresql --database \"user=postgres dbname=step_ca\"\n", name)
57+
fmt.Fprintln(os.Stderr, "\nOptions:")
58+
fs.PrintDefaults()
59+
}
60+
61+
func main() {
62+
var v1, v2 bool
63+
var dir, valueDir string
64+
var typ, database string
65+
66+
fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
67+
68+
fs.BoolVar(&v1, "v1", false, "use badger v1 as the source database")
69+
fs.BoolVar(&v2, "v2", true, "use badger v2 as the source database")
70+
fs.StringVar(&dir, "dir", "", "badger database directory")
71+
fs.StringVar(&valueDir, "value-dir", "", "badger database value directory")
72+
fs.StringVar(&typ, "type", "", "the destination database type to use")
73+
fs.StringVar(&database, "database", "", "the destination driver-specific data source name")
74+
fs.Usage = func() { usage(fs) }
75+
fs.Parse(os.Args[1:])
76+
77+
switch {
78+
case v1 == v2:
79+
fatal("flag --v1 or --v2 are required")
80+
case dir == "":
81+
fatal("flag --dir is required")
82+
case typ != "postgresql" && typ != "mysql":
83+
fatal(`flag --type must be "postgresql" or "mysql"`)
84+
case database == "":
85+
fatal("flag --database required")
86+
}
87+
88+
var (
89+
err error
90+
v1DB *badgerv1.DB
91+
v2DB *badgerv2.DB
92+
)
93+
94+
if v1 {
95+
if v1DB, err = badgerV1Open(dir, valueDir); err != nil {
96+
fatal("error opening badger v2 database: %v", err)
97+
}
98+
} else {
99+
if v2DB, err = badgerV2Open("/tmp/db", ""); err != nil {
100+
fatal("error opening badger v2 database: %v", err)
101+
}
102+
}
103+
104+
db, err := nosql.New(typ, database)
105+
if err != nil {
106+
fatal("error opening %s database: %v", typ, err)
107+
}
108+
109+
allTables := append([]string{}, authorityTables...)
110+
allTables = append(allTables, acmeTables...)
111+
112+
for _, table := range allTables {
113+
var n int64
114+
fmt.Printf("migrating %s ...\n", table)
115+
if err := db.CreateTable([]byte(table)); err != nil {
116+
fatal("error creating table %s: %v", table, err)
117+
}
118+
119+
if v1 {
120+
if err := badgerV1Iterate(v1DB, []byte(table), func(bucket, key, value []byte) error {
121+
n++
122+
return db.Set(bucket, key, value)
123+
}); err != nil {
124+
fatal("error inserting into %s: %v", table, err)
125+
}
126+
} else {
127+
if err := badgerV2Iterate(v2DB, []byte(table), func(bucket, key, value []byte) error {
128+
n++
129+
return db.Set(bucket, key, value)
130+
}); err != nil {
131+
fatal("error inserting into %s: %v", table, err)
132+
}
133+
}
134+
135+
log.Printf("%d rows\n", n)
136+
}
137+
}
138+
139+
func fatal(format string, args ...any) {
140+
fmt.Fprintf(os.Stderr, format, args...)
141+
fmt.Fprintln(os.Stderr)
142+
os.Exit(1)
143+
}
144+
145+
func badgerV1Open(dir, valueDir string) (*badgerv1.DB, error) {
146+
opts := badgerv1.DefaultOptions(dir)
147+
if valueDir != "" {
148+
opts.ValueDir = valueDir
149+
}
150+
return badgerv1.Open(opts)
151+
}
152+
153+
func badgerV2Open(dir, valueDir string) (*badgerv2.DB, error) {
154+
opts := badgerv2.DefaultOptions(dir)
155+
if valueDir != "" {
156+
opts.ValueDir = valueDir
157+
}
158+
return badgerv2.Open(opts)
159+
}
160+
161+
func badgerV1Iterate(db *badgerv1.DB, table []byte, fn func(table, key, value []byte) error) error {
162+
return db.View(func(txn *badgerv1.Txn) error {
163+
var tableExists bool
164+
165+
it := txn.NewIterator(badgerv1.DefaultIteratorOptions)
166+
defer it.Close()
167+
168+
prefix, err := badgerEncode(table)
169+
if err != nil {
170+
return err
171+
}
172+
173+
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
174+
tableExists = true
175+
item := it.Item()
176+
bk := item.KeyCopy(nil)
177+
if isBadgerTable(bk) {
178+
continue
179+
}
180+
181+
bucket, key, err := fromBadgerKey(bk)
182+
if err != nil {
183+
return fmt.Errorf("error converting from badger key %s", bk)
184+
}
185+
if !bytes.Equal(table, bucket) {
186+
return fmt.Errorf("bucket names do not match; want %s, but got %s", table, bucket)
187+
}
188+
189+
v, err := item.ValueCopy(nil)
190+
if err != nil {
191+
return fmt.Errorf("error retrieving contents from database value: %w", err)
192+
}
193+
value := cloneBytes(v)
194+
195+
if err := fn(bucket, key, value); err != nil {
196+
return fmt.Errorf("error exporting %s[%s]=%v", table, key, value)
197+
}
198+
}
199+
200+
if !tableExists {
201+
fmt.Printf("bucket %s not found\n", table)
202+
}
203+
204+
return nil
205+
})
206+
}
207+
208+
func badgerV2Iterate(db *badgerv2.DB, table []byte, fn func(table, key, value []byte) error) error {
209+
return db.View(func(txn *badgerv2.Txn) error {
210+
var tableExists bool
211+
212+
it := txn.NewIterator(badgerv2.DefaultIteratorOptions)
213+
defer it.Close()
214+
215+
prefix, err := badgerEncode(table)
216+
if err != nil {
217+
return err
218+
}
219+
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
220+
tableExists = true
221+
item := it.Item()
222+
bk := item.KeyCopy(nil)
223+
if isBadgerTable(bk) {
224+
continue
225+
}
226+
227+
bucket, key, err := fromBadgerKey(bk)
228+
if err != nil {
229+
return fmt.Errorf("error converting from badgerKey %s: %w", bk, err)
230+
}
231+
if !bytes.Equal(table, bucket) {
232+
return fmt.Errorf("bucket names do not match; want %s, but got %s", table, bucket)
233+
}
234+
235+
v, err := item.ValueCopy(nil)
236+
if err != nil {
237+
return fmt.Errorf("error retrieving contents from database value: %w", err)
238+
}
239+
value := cloneBytes(v)
240+
241+
if err := fn(bucket, key, value); err != nil {
242+
return fmt.Errorf("error exporting %s[%s]=%v", table, key, value)
243+
}
244+
}
245+
if !tableExists {
246+
log.Printf("bucket %s not found", table)
247+
}
248+
return nil
249+
})
250+
}
251+
252+
// badgerEncode encodes a byte slice into a section of a BadgerKey.
253+
// See documentation for toBadgerKey.
254+
func badgerEncode(val []byte) ([]byte, error) {
255+
l := len(val)
256+
switch {
257+
case l == 0:
258+
return nil, errors.New("input cannot be empty")
259+
case l > 65535:
260+
return nil, errors.New("length of input cannot be greater than 65535")
261+
default:
262+
lb := new(bytes.Buffer)
263+
if err := binary.Write(lb, binary.LittleEndian, uint16(l)); err != nil {
264+
return nil, fmt.Errorf("error doing binary Write: %w", err)
265+
}
266+
return append(lb.Bytes(), val...), nil
267+
}
268+
}
269+
270+
// isBadgerTable returns True if the slice is a badgerTable token, false otherwise.
271+
// badgerTable means that the slice contains only the [size|value] of one section
272+
// of a badgerKey and no remainder. A badgerKey is [buket|key], while a badgerTable
273+
// is only the bucket section.
274+
func isBadgerTable(bk []byte) bool {
275+
if k, rest := parseBadgerEncode(bk); len(k) > 0 && len(rest) == 0 {
276+
return true
277+
}
278+
return false
279+
}
280+
281+
// fromBadgerKey returns the bucket and key encoded in a BadgerKey.
282+
// See documentation for toBadgerKey.
283+
func fromBadgerKey(bk []byte) ([]byte, []byte, error) {
284+
bucket, rest := parseBadgerEncode(bk)
285+
if len(bucket) == 0 || len(rest) == 0 {
286+
return nil, nil, fmt.Errorf("invalid badger key: %v", bk)
287+
}
288+
289+
key, rest2 := parseBadgerEncode(rest)
290+
if len(key) == 0 || len(rest2) != 0 {
291+
return nil, nil, fmt.Errorf("invalid badger key: %v", bk)
292+
}
293+
294+
return bucket, key, nil
295+
}
296+
297+
// cloneBytes returns a copy of a given slice.
298+
func cloneBytes(v []byte) []byte {
299+
var clone = make([]byte, len(v))
300+
copy(clone, v)
301+
return clone
302+
}
303+
304+
func parseBadgerEncode(bk []byte) (value, rest []byte) {
305+
var (
306+
keyLen uint16
307+
start = uint16(2)
308+
length = uint16(len(bk))
309+
)
310+
if uint16(len(bk)) < start {
311+
return nil, bk
312+
}
313+
// First 2 bytes stores the length of the value.
314+
if err := binary.Read(bytes.NewReader(bk[:2]), binary.LittleEndian, &keyLen); err != nil {
315+
return nil, bk
316+
}
317+
end := start + keyLen
318+
switch {
319+
case length < end:
320+
return nil, bk
321+
case length == end:
322+
return bk[start:end], nil
323+
default:
324+
return bk[start:end], bk[end:]
325+
}
326+
}

0 commit comments

Comments
 (0)