Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

record restic restore progress in PodVolumeRestore #1854

Merged
merged 7 commits into from
Sep 10, 2019
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
1 change: 1 addition & 0 deletions changelogs/unreleased/1854-prydonius
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
report restore progress in PodVolumeRestores and expose progress in the velero restore describe --details command
5 changes: 5 additions & 0 deletions pkg/apis/velero/v1/pod_volume_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ type PodVolumeRestoreStatus struct {
// Completion time is recorded even on failed restores.
// The server's time is used for CompletionTimestamps
CompletionTimestamp metav1.Time `json:"completionTimestamp"`

// Progress holds the total number of bytes of the snapshot and the current
// number of restored bytes. This can be used to display progress information
// about the restore operation.
Progress PodVolumeOperationProgress `json:"progress"`
}

// +genclient
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/velero/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pkg/cmd/util/output/restore_describer.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, det
restoresByPod := new(volumesByPod)

for _, restore := range restoresByPhase[phase] {
// TODO(adnan): replace last parameter with progress from status (#1749)
restoresByPod.Add(restore.Spec.Pod.Namespace, restore.Spec.Pod.Name, restore.Spec.Volume, phase, v1.PodVolumeOperationProgress{})
restoresByPod.Add(restore.Spec.Pod.Namespace, restore.Spec.Pod.Name, restore.Spec.Volume, phase, restore.Status.Progress)
}

d.Printf("\t%s:\n", phase)
Expand Down
15 changes: 13 additions & 2 deletions pkg/controller/pod_volume_restore_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import (
listers "github.com/heptio/velero/pkg/generated/listers/velero/v1"
"github.com/heptio/velero/pkg/restic"
"github.com/heptio/velero/pkg/util/boolptr"
veleroexec "github.com/heptio/velero/pkg/util/exec"
"github.com/heptio/velero/pkg/util/filesystem"
"github.com/heptio/velero/pkg/util/kube"
)
Expand Down Expand Up @@ -339,7 +338,7 @@ func (c *podVolumeRestoreController) restorePodVolume(req *velerov1api.PodVolume

var stdout, stderr string

if stdout, stderr, err = veleroexec.RunCommand(resticCmd.Cmd()); err != nil {
if stdout, stderr, err = restic.RunRestore(resticCmd, log, c.updateRestoreProgressFunc(req, log)); err != nil {
return errors.Wrapf(err, "error running restic restore, cmd=%s, stdout=%s, stderr=%s", resticCmd.String(), stdout, stderr)
}
log.Debugf("Ran command=%s, stdout=%s, stderr=%s", resticCmd.String(), stdout, stderr)
Expand Down Expand Up @@ -416,3 +415,15 @@ func (c *podVolumeRestoreController) failRestore(req *velerov1api.PodVolumeResto
}
return nil
}

// updateRestoreProgressFunc returns a func that takes progress info and patches
// the PVR with the new progress
func (c *podVolumeRestoreController) updateRestoreProgressFunc(req *velerov1api.PodVolumeRestore, log logrus.FieldLogger) func(velerov1api.PodVolumeOperationProgress) {
return func(progress velerov1api.PodVolumeOperationProgress) {
if _, err := c.patchPodVolumeRestore(req, func(r *velerov1api.PodVolumeRestore) {
r.Status.Progress = progress
}); err != nil {
log.WithError(err).Error("error updating PodVolumeRestore progress")
}
}
}
10 changes: 10 additions & 0 deletions pkg/restic/command_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,13 @@ func UnlockCommand(repoIdentifier string) *Command {
RepoIdentifier: repoIdentifier,
}
}

func StatsCommand(repoIdentifier, passwordFile, snapshotID string) *Command {
return &Command{
Command: "stats",
RepoIdentifier: repoIdentifier,
PasswordFile: passwordFile,
Args: []string{snapshotID},
ExtraFlags: []string{"--json"},
}
}
10 changes: 10 additions & 0 deletions pkg/restic/command_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,13 @@ func TestForgetCommand(t *testing.T) {
assert.Equal(t, "repo-id", c.RepoIdentifier)
assert.Equal(t, []string{"snapshot-id"}, c.Args)
}

func TestStatsCommand(t *testing.T) {
c := StatsCommand("repo-id", "password-file", "snapshot-id")

assert.Equal(t, "stats", c.Command)
assert.Equal(t, "repo-id", c.RepoIdentifier)
assert.Equal(t, "password-file", c.PasswordFile)
assert.Equal(t, []string{"snapshot-id"}, c.Args)
assert.Equal(t, []string{"--json"}, c.ExtraFlags)
}
99 changes: 99 additions & 0 deletions pkg/restic/exec_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ import (

velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/util/exec"
"github.com/heptio/velero/pkg/util/filesystem"
)

const restoreProgressCheckInterval = 10 * time.Second
const backupProgressCheckInterval = 10 * time.Second

var fileSystem = filesystem.NewFileSystem()

type backupStatusLine struct {
MessageType string `json:"message_type"`
// seen in status lines
Expand Down Expand Up @@ -171,3 +175,98 @@ func getSummaryLine(b []byte) ([]byte, error) {
}
return b[summaryLineIdx : summaryLineIdx+newLineIdx], nil
}

// RunRestore runs a `restic restore` command and monitors the volume size to
// provide progress updates to the caller.
func RunRestore(restoreCmd *Command, log logrus.FieldLogger, updateFunc func(velerov1api.PodVolumeOperationProgress)) (string, string, error) {
snapshotSize, err := getSnapshotSize(restoreCmd.RepoIdentifier, restoreCmd.PasswordFile, restoreCmd.Args[0])
if err != nil {
return "", "", err
}

updateFunc(velerov1api.PodVolumeOperationProgress{
TotalBytes: snapshotSize,
})

// create a channel to signal when to end the goroutine scanning for progress
// updates
quit := make(chan struct{})

go func() {
ticker := time.NewTicker(restoreProgressCheckInterval)
for {
select {
case <-ticker.C:
volumeSize, err := getVolumeSize(restoreCmd.Dir)
if err != nil {
log.WithError(err).Errorf("error getting restic restore progress")
}

updateFunc(velerov1api.PodVolumeOperationProgress{
TotalBytes: snapshotSize,
BytesDone: volumeSize,
})
case <-quit:
ticker.Stop()
return
}
}
}()

stdout, stderr, err := exec.RunCommand(restoreCmd.Cmd())
quit <- struct{}{}

// update progress to 100%
updateFunc(velerov1api.PodVolumeOperationProgress{
TotalBytes: snapshotSize,
BytesDone: snapshotSize,
})

return stdout, stderr, err
}

func getSnapshotSize(repoIdentifier, passwordFile, snapshotID string) (int64, error) {
cmd := StatsCommand(repoIdentifier, passwordFile, snapshotID)

stdout, stderr, err := exec.RunCommand(cmd.Cmd())
if err != nil {
return 0, errors.Wrapf(err, "error running command, stderr=%s", stderr)
}

var snapshotStats struct {
TotalSize int64 `json:"total_size"`
}

if err := json.Unmarshal([]byte(stdout), &snapshotStats); err != nil {
return 0, errors.Wrap(err, "error unmarshalling restic stats result")
}

if snapshotStats.TotalSize == 0 {
return 0, errors.Errorf("error getting snapshot size %+v", snapshotStats)
}

return snapshotStats.TotalSize, nil
}

func getVolumeSize(path string) (int64, error) {
var size int64

files, err := fileSystem.ReadDir(path)
if err != nil {
return 0, errors.Wrapf(err, "error reading directory %s", path)
}

for _, file := range files {
if file.IsDir() {
s, err := getVolumeSize(fmt.Sprintf("%s/%s", path, file.Name()))
if err != nil {
return 0, err
}
size += s
} else {
size += file.Size()
}
}

return size, nil
}
28 changes: 28 additions & 0 deletions pkg/restic/exec_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/heptio/velero/pkg/test"
"github.com/heptio/velero/pkg/util/filesystem"
)

func Test_getSummaryLine(t *testing.T) {
Expand Down Expand Up @@ -78,3 +81,28 @@ third line
})
}
}

func Test_getVolumeSize(t *testing.T) {
files := map[string][]byte{
"/file1.txt": []byte("file1"),
"/file2.txt": []byte("file2"),
"/file3.txt": []byte("file3"),
"/files/file4.txt": []byte("file4"),
"/files/nested/file5.txt": []byte("file5"),
}
fakefs := test.NewFakeFileSystem()

var expectedSize int64
for path, content := range files {
fakefs.WithFile(path, content)
expectedSize += int64(len(content))
}

fileSystem = fakefs
defer func() { fileSystem = filesystem.NewFileSystem() }()

actualSize, err := getVolumeSize("/")

assert.NoError(t, err)
assert.Equal(t, expectedSize, actualSize)
}