Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions go/cmd/vtctldclient/internal/command/backups.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,50 @@ import (
"github.com/spf13/cobra"

"vitess.io/vitess/go/cmd/vtctldclient/cli"
"vitess.io/vitess/go/vt/topo/topoproto"

vtctldatapb "vitess.io/vitess/go/vt/proto/vtctldata"
)

// GetBackups makes a GetBackups gRPC call to a vtctld.
var GetBackups = &cobra.Command{
Use: "GetBackups keyspace shard",
Args: cobra.ExactArgs(2),
Use: "GetBackups <keyspace/shard>",
Args: cobra.ExactArgs(1),
RunE: commandGetBackups,
}

var getBackupsOptions = struct {
Limit uint32
OutputJSON bool
}{}

func commandGetBackups(cmd *cobra.Command, args []string) error {
cli.FinishedParsing(cmd)
keyspace, shard, err := topoproto.ParseKeyspaceShard(cmd.Flags().Arg(0))
if err != nil {
return err
}

keyspace := cmd.Flags().Arg(0)
shard := cmd.Flags().Arg(1)
cli.FinishedParsing(cmd)

resp, err := client.GetBackups(commandCtx, &vtctldatapb.GetBackupsRequest{
Keyspace: keyspace,
Shard: shard,
Limit: getBackupsOptions.Limit,
})
if err != nil {
return err
}

if getBackupsOptions.OutputJSON {
data, err := cli.MarshalJSON(resp)
if err != nil {
return err
}

fmt.Printf("%s\n", data)
return nil
}

names := make([]string, len(resp.Backups))
for i, b := range resp.Backups {
names[i] = b.Name
Expand All @@ -58,5 +78,7 @@ func commandGetBackups(cmd *cobra.Command, args []string) error {
}

func init() {
GetBackups.Flags().Uint32VarP(&getBackupsOptions.Limit, "limit", "l", 0, "Retrieve only the most recent N backups")
GetBackups.Flags().BoolVarP(&getBackupsOptions.OutputJSON, "json", "j", false, "Output backup info in JSON format rather than a list of backups")
Root.AddCommand(GetBackups)
}
44 changes: 44 additions & 0 deletions go/protoutil/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
Copyright 2021 The Vitess 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 protoutil

import (
"time"

"vitess.io/vitess/go/vt/proto/vttime"
)

// TimeFromProto converts a vttime.Time proto message into a time.Time object.
func TimeFromProto(tpb *vttime.Time) time.Time {
if tpb == nil {
return time.Time{}
}

return time.Unix(tpb.Seconds, int64(tpb.Nanoseconds))
}

// TimeToProto converts a time.Time object into a vttime.Time proto mesasge.
func TimeToProto(t time.Time) *vttime.Time {
secs, nanos := t.Unix(), t.UnixNano()

nsecs := secs * 1e9
extraNanos := nanos - nsecs
return &vttime.Time{
Seconds: secs,
Nanoseconds: int32(extraNanos),
}
}
52 changes: 52 additions & 0 deletions go/protoutil/time_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2021 The Vitess 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 protoutil

import (
"testing"
"time"

"github.com/stretchr/testify/assert"

"vitess.io/vitess/go/test/utils"
"vitess.io/vitess/go/vt/proto/vttime"
)

func TestTimeFromProto(t *testing.T) {
now := time.Date(2021, time.June, 12, 13, 14, 15, 0 /* nanos */, time.UTC)
vtt := TimeToProto(now)

utils.MustMatch(t, now, TimeFromProto(vtt))

vtt.Nanoseconds = 100
utils.MustMatch(t, now.Add(100*time.Nanosecond), TimeFromProto(vtt))

vtt.Nanoseconds = 1e9
utils.MustMatch(t, now.Add(time.Second), TimeFromProto(vtt))

assert.True(t, TimeFromProto(nil).IsZero(), "expected Go time from nil vttime to be Zero")
}

func TestTimeToProto(t *testing.T) {
now := time.Date(2021, time.June, 12, 13, 14, 15, 0 /* nanos */, time.UTC)
secs := now.Unix()
utils.MustMatch(t, &vttime.Time{Seconds: secs}, TimeToProto(now))

// Testing secs/nanos conversions
utils.MustMatch(t, &vttime.Time{Seconds: secs, Nanoseconds: 100}, TimeToProto(now.Add(100*time.Nanosecond)))
utils.MustMatch(t, &vttime.Time{Seconds: secs + 1}, TimeToProto(now.Add(1e9*time.Nanosecond))) // this should rollover to a full second
}
36 changes: 36 additions & 0 deletions go/vt/mysqlctl/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import (
"vitess.io/vitess/go/vt/log"
"vitess.io/vitess/go/vt/mysqlctl/backupstorage"
"vitess.io/vitess/go/vt/proto/vtrpc"
"vitess.io/vitess/go/vt/topo/topoproto"
"vitess.io/vitess/go/vt/vterrors"

topodatapb "vitess.io/vitess/go/vt/proto/topodata"
)

// This file handles the backup and restore related code
Expand Down Expand Up @@ -138,6 +141,39 @@ func Backup(ctx context.Context, params BackupParams) error {
return finishErr
}

// ParseBackupName parses the backup name for a given dir/name, according to
// the format generated by mysqlctl.Backup. An error is returned only if the
// backup name does not have the expected number of parts; errors parsing the
// timestamp and tablet alias are logged, and a nil value is returned for those
// fields in case of error.
func ParseBackupName(dir string, name string) (backupTime *time.Time, alias *topodatapb.TabletAlias, err error) {
parts := strings.Split(name, ".")
if len(parts) != 3 {
return nil, nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "cannot backup name %s, expected <date>.<time>.<tablet_alias>", name)
}

// parts[0]: date part of BackupTimestampFormat
// parts[1]: time part of BackupTimestampFormat
// parts[2]: tablet alias
timestamp := strings.Join(parts[:2], ".")
aliasStr := parts[2]

btime, err := time.Parse(BackupTimestampFormat, timestamp)
if err != nil {
log.Errorf("error parsing backup time for %s/%s: %s", dir, name, err)
} else {
backupTime = &btime
}

alias, err = topoproto.ParseTabletAlias(aliasStr)
if err != nil {
log.Errorf("error parsing tablet alias for %s/%s: %s", dir, name, err)
alias = nil
}

return backupTime, alias, nil
}

// checkNoDB makes sure there is no user data already there.
// Used by Restore, as we do not want to destroy an existing DB.
// The user's database name must be given since we ignore all others.
Expand Down
17 changes: 16 additions & 1 deletion go/vt/mysqlctl/mysqlctlproto/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,30 @@ limitations under the License.
package mysqlctlproto

import (
"vitess.io/vitess/go/protoutil"
"vitess.io/vitess/go/vt/mysqlctl"
"vitess.io/vitess/go/vt/mysqlctl/backupstorage"

mysqlctlpb "vitess.io/vitess/go/vt/proto/mysqlctl"
)

// BackupHandleToProto returns a BackupInfo proto from a BackupHandle.
func BackupHandleToProto(bh backupstorage.BackupHandle) *mysqlctlpb.BackupInfo {
return &mysqlctlpb.BackupInfo{
bi := &mysqlctlpb.BackupInfo{
Name: bh.Name(),
Directory: bh.Directory(),
}

btime, alias, err := mysqlctl.ParseBackupName(bi.Directory, bi.Name)
if err != nil { // if bi.Name does not match expected format, don't parse any further fields
return bi
}

if btime != nil {
bi.Time = protoutil.TimeToProto(*btime)
}

bi.TabletAlias = alias

return bi
}
114 changes: 114 additions & 0 deletions go/vt/mysqlctl/mysqlctlproto/backup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Copyright 2021 The Vitess 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 mysqlctlproto

import (
"path"
"testing"
"time"

"vitess.io/vitess/go/protoutil"
"vitess.io/vitess/go/test/utils"
"vitess.io/vitess/go/vt/mysqlctl/backupstorage"

mysqlctlpb "vitess.io/vitess/go/vt/proto/mysqlctl"
topodatapb "vitess.io/vitess/go/vt/proto/topodata"
)

type backupHandle struct {
backupstorage.BackupHandle
name string
directory string
}

func (bh *backupHandle) Name() string { return bh.name }
func (bh *backupHandle) Directory() string { return bh.directory }
func (bh *backupHandle) testname() string { return path.Join(bh.directory, bh.name) }

func TestBackupHandleToProto(t *testing.T) {
t.Parallel()

now := time.Date(2021, time.June, 12, 15, 4, 5, 0, time.UTC)
tests := []struct {
bh *backupHandle
want *mysqlctlpb.BackupInfo
}{
{
bh: &backupHandle{
name: "2021-06-12.150405.zone1-100",
directory: "foo",
},
want: &mysqlctlpb.BackupInfo{
Name: "2021-06-12.150405.zone1-100",
Directory: "foo",
TabletAlias: &topodatapb.TabletAlias{
Cell: "zone1",
Uid: 100,
},
Time: protoutil.TimeToProto(now),
},
},
{
bh: &backupHandle{
name: "bar",
directory: "foo",
},
want: &mysqlctlpb.BackupInfo{
Name: "bar",
Directory: "foo",
},
},
{
bh: &backupHandle{
name: "invalid.time.zone1-100",
directory: "foo",
},
want: &mysqlctlpb.BackupInfo{
Name: "invalid.time.zone1-100",
Directory: "foo",
TabletAlias: &topodatapb.TabletAlias{
Cell: "zone1",
Uid: 100,
},
Time: nil,
},
},
{
bh: &backupHandle{
name: "2021-06-12.150405.not_an_alias",
directory: "foo",
},
want: &mysqlctlpb.BackupInfo{
Name: "2021-06-12.150405.not_an_alias",
Directory: "foo",
TabletAlias: nil,
Time: protoutil.TimeToProto(now),
},
},
}

for _, tt := range tests {
tt := tt

t.Run(tt.bh.testname(), func(t *testing.T) {
t.Parallel()

got := BackupHandleToProto(tt.bh)
utils.MustMatch(t, tt.want, got)
})
}
}
Loading