Skip to content

Commit

Permalink
etcdutl: Implement migrate --force command
Browse files Browse the repository at this point in the history
  • Loading branch information
serathius committed Jul 16, 2021
1 parent 28f86ee commit e9c9433
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-3.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0).
### etcdutl v3

- Add command to generate [shell completion](https://github.com/etcd-io/etcd/pull/13142).
- Add `migrate` command for downgrading/upgrading etcd data dir files.

### Package `server`

Expand Down
1 change: 1 addition & 0 deletions etcdutl/ctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func init() {
etcdutl.NewSnapshotCommand(),
etcdutl.NewVersionCommand(),
etcdutl.NewCompletionCommand(),
etcdutl.NewMigrateCommand(),
)
}

Expand Down
126 changes: 126 additions & 0 deletions etcdutl/etcdutl/migrate_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2021 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package etcdutl

import (
"fmt"
"github.com/coreos/go-semver/semver"
"github.com/spf13/cobra"
"go.etcd.io/etcd/pkg/v3/cobrautl"
"go.etcd.io/etcd/server/v3/datadir"
"go.etcd.io/etcd/server/v3/storage/backend"
"go.etcd.io/etcd/server/v3/storage/schema"
"go.uber.org/zap"
"strings"
)

// NewMigrateCommand prints out the version of etcd.
func NewMigrateCommand() *cobra.Command {
o := newMigrateOptions()
cmd := &cobra.Command{
Use: "migrate",
Short: "Migrates schema of etcd data dir files to make them compatible with different etcd version",
Run: func(cmd *cobra.Command, args []string) {
cfg, err := o.Config()
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
}
err = migrateCommandFunc(cfg)
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitError, err)
}
},
}
o.AddFlags(cmd)
return cmd
}

type migrateOptions struct {
dataDir string
targetVersion string
force bool
}

func newMigrateOptions() *migrateOptions {
return &migrateOptions{
dataDir: "",
targetVersion: "",
force: false,
}
}

func (o *migrateOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.dataDir, "data-dir", o.dataDir, "Path to the etcd data dir")
cmd.MarkFlagRequired("data-dir")
cmd.MarkFlagDirname("data-dir")

cmd.Flags().StringVar(&o.targetVersion, "target-version", o.targetVersion, "Target version, format X.Y for example 3.6")
cmd.MarkFlagRequired("target-version")

cmd.Flags().BoolVar(&o.force, "force", o.force, "Ignore migration failure and forcefully override storage version. `")
}

func (o *migrateOptions) Config() (*migrateConfig, error) {
c := &migrateConfig{
force: o.force,
}
var err error
dotCount := strings.Count(o.targetVersion, ".")
if dotCount != 1 {
return nil, fmt.Errorf(`wrong target-version format, expected "3.6", got %q`, o.targetVersion)
}
c.targetVersion, err = semver.NewVersion(o.targetVersion + ".0")
if err != nil {
return nil, fmt.Errorf("failed to parse target-version: %w", err)
}

dbPath := datadir.ToBackendFileName(o.dataDir)
c.be = backend.NewDefaultBackend(dbPath)

return c, nil
}

type migrateConfig struct {
be backend.Backend
targetVersion *semver.Version
force bool
}

func migrateCommandFunc(c *migrateConfig) error {
defer c.be.Close()
lg := GetLogger()
tx := c.be.BatchTx()
tx.Lock()
current, err := schema.DetectSchemaVersion(lg, tx)
if err != nil {
tx.Unlock()
lg.Error("failed to detect storage version. Please make sure you are using data dir from etcd v3.5 and older")
return err
}
if *current == *c.targetVersion {
tx.Unlock()
lg.Info("storage version up-to-date", zap.String("storage-version", current.String()))
return nil
}
if !c.force {
tx.Unlock()
return fmt.Errorf("storage version migration is not yet supported")
}
schema.UnsafeSetStorageVersion(tx, c.targetVersion)
tx.Unlock()
c.be.ForceCommit()
lg.Warn("forcfully set storage version", zap.String("storage-version", c.targetVersion.String()))
return nil
}
1 change: 1 addition & 0 deletions etcdutl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ replace (
)

require (
github.com/coreos/go-semver v0.3.0 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.1.3
Expand Down
4 changes: 2 additions & 2 deletions server/storage/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var (
func UpdateStorageSchema(lg *zap.Logger, tx backend.BatchTx) error {
tx.Lock()
defer tx.Unlock()
v, err := detectStorageVersion(lg, tx)
v, err := DetectSchemaVersion(lg, tx)
if err != nil {
return fmt.Errorf("cannot determine storage version: %w", err)
}
Expand All @@ -48,7 +48,7 @@ func UpdateStorageSchema(lg *zap.Logger, tx backend.BatchTx) error {
return nil
}

func detectStorageVersion(lg *zap.Logger, tx backend.ReadTx) (*semver.Version, error) {
func DetectSchemaVersion(lg *zap.Logger, tx backend.ReadTx) (*semver.Version, error) {
v := UnsafeReadStorageVersion(tx)
if v != nil {
return v, nil
Expand Down
103 changes: 103 additions & 0 deletions tests/e2e/utl_migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package e2e

import (
"fmt"
"go.etcd.io/etcd/client/pkg/v3/fileutil"
"strings"
"testing"
"time"
)

func TestEtctlutlMigrate(t *testing.T) {
lastReleaseBinary := binDir + "/etcd-last-release"

tcs := []struct {
name string
targetVersion string
binary string
force bool

expectSubString string
}{
{
name: "Migrate v3.5 to v3.5 is no-op",
binary: lastReleaseBinary,
targetVersion: "3.5",
expectSubString: "storage version up-to-date\t" + `{"storage-version": "3.5.0"}`,
},
{
name: "Upgrade v3.5 to v3.6 should fail until it's implemented",
binary: lastReleaseBinary,
targetVersion: "3.6",
expectSubString: "storage version migration is not yet supported",
},
{
name: "Migrate v3.6 to 3.6 is no-op",
targetVersion: "3.6",
expectSubString: "storage version up-to-date\t" + `{"storage-version": "3.6.0"}`,
},
{
name: "Downgrade v3.6 to 3.5 should fail until it's implemented",
targetVersion: "3.5",
expectSubString: "storage version migration is not yet supported",
},
{
name: "Downgrade v3.6 to 3.5 with force should work",
targetVersion: "3.5",
force: true,
expectSubString: "forcfully set storage version\t" + `{"storage-version": "3.5.0"}`,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
BeforeTest(t)
if tc.binary != "" && !fileutil.Exist(tc.binary) {
t.Skipf("%q does not exist", lastReleaseBinary)
}
dataDirPath := t.TempDir()

epc, err := newEtcdProcessCluster(t, &etcdProcessClusterConfig{
execPath: tc.binary,
dataDirPath: dataDirPath,
clusterSize: 1,
initialToken: "new",
keepDataDir: true,
// Set low snapshotCount to ensure wal snapshot is done
snapshotCount: 1,
})
if err != nil {
t.Fatalf("could not start etcd process cluster (%v)", err)
}
defer func() {
if errC := epc.Close(); errC != nil {
t.Fatalf("error closing etcd processes (%v)", errC)
}
}()

dialTimeout := 10 * time.Second
prefixArgs := []string{ctlBinPath, "--endpoints", strings.Join(epc.EndpointsV3(), ","), "--dial-timeout", dialTimeout.String()}

t.Log("Write keys to ensure wal snapshot is created and all v3.5 fields are set...")
for i := 0; i < 10; i++ {
if err = spawnWithExpect(append(prefixArgs, "put", fmt.Sprintf("%d", i), "value"), "OK"); err != nil {
t.Fatal(err)
}
}

t.Log("Stopping the server...")
if err = epc.procs[0].Stop(); err != nil {
t.Fatal(err)
}

t.Log("etcdutl migrate...")
args := []string{utlBinPath, "migrate", "--data-dir", dataDirPath, "--target-version", tc.targetVersion}
if tc.force {
args = append(args, "--force")
}
err = spawnWithExpect(args, tc.expectSubString)
if err != nil {
t.Fatal(err)
}
})
}
}

0 comments on commit e9c9433

Please sign in to comment.