Add StartTimestamp and CompletionTimestamp in Restore Status (#2748)

Signed-off-by: thejas <thejasb99@gmail.com>
This commit is contained in:
Thejas Babu
2020-07-23 00:10:39 +05:30
committed by GitHub
parent c27c3cd56a
commit d0d143e119
10 changed files with 121 additions and 23 deletions

View File

@@ -0,0 +1,2 @@
Adds Start and CompletionTimestamp to RestoreStatus
Displays the Timestamps when issued a print or describe

View File

@@ -143,6 +143,13 @@ spec:
status:
description: RestoreStatus captures the current status of a Velero restore
properties:
completionTimestamp:
description: CompletionTimestamp records the time the restore operation
was completed. Completion time is recorded even on failed restore.
The server's time is used for StartTimestamps
format: date-time
nullable: true
type: string
errors:
description: Errors is a count of all error messages that were generated
during execution of the restore. The actual errors are stored in object
@@ -162,6 +169,12 @@ spec:
- PartiallyFailed
- Failed
type: string
startTimestamp:
description: StartTimestamp records the time the restore operation was
started. The server's time is used for StartTimestamps
format: date-time
nullable: true
type: string
validationErrors:
description: ValidationErrors is a slice of all validation errors (if
applicable)

File diff suppressed because one or more lines are too long

View File

@@ -137,6 +137,19 @@ type RestoreStatus struct {
// FailureReason is an error that caused the entire restore to fail.
// +optional
FailureReason string `json:"failureReason,omitempty"`
// StartTimestamp records the time the restore operation was started.
// The server's time is used for StartTimestamps
// +optional
// +nullable
StartTimestamp *metav1.Time `json:"startTimestamp,omitempty"`
// CompletionTimestamp records the time the restore operation was completed.
// Completion time is recorded even on failed restore.
// The server's time is used for StartTimestamps
// +optional
// +nullable
CompletionTimestamp *metav1.Time `json:"completionTimestamp,omitempty"`
}
// +genclient

View File

@@ -1146,6 +1146,14 @@ func (in *RestoreStatus) DeepCopyInto(out *RestoreStatus) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.StartTimestamp != nil {
in, out := &in.StartTimestamp, &out.StartTimestamp
*out = (*in).DeepCopy()
}
if in.CompletionTimestamp != nil {
in, out := &in.CompletionTimestamp, &out.CompletionTimestamp
*out = (*in).DeepCopy()
}
return
}

View File

@@ -17,6 +17,8 @@ limitations under the License.
package builder
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
@@ -133,3 +135,15 @@ func (b *RestoreBuilder) RestorePVs(val bool) *RestoreBuilder {
b.object.Spec.RestorePVs = &val
return b
}
// StartTimestamp sets the Restore's start timestamp.
func (b *RestoreBuilder) StartTimestamp(val time.Time) *RestoreBuilder {
b.object.Status.StartTimestamp = &metav1.Time{Time: val}
return b
}
// CompletionTimestamp sets the Restore's completion timestamp.
func (b *RestoreBuilder) CompletionTimestamp(val time.Time) *RestoreBuilder {
b.object.Status.CompletionTimestamp = &metav1.Time{Time: val}
return b
}

View File

@@ -48,6 +48,20 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor
d.Printf("Phase:\t%s%s\n", restore.Status.Phase, resultsNote)
d.Println()
// "<n/a>" output should only be applicable for restore that failed validation
if restore.Status.StartTimestamp == nil || restore.Status.StartTimestamp.IsZero() {
d.Printf("Started:\t%s\n", "<n/a>")
} else {
d.Printf("Started:\t%s\n", restore.Status.StartTimestamp)
}
if restore.Status.CompletionTimestamp == nil || restore.Status.CompletionTimestamp.IsZero() {
d.Printf("Completed:\t%s\n", "<n/a>")
} else {
d.Printf("Completed:\t%s\n", restore.Status.CompletionTimestamp)
}
if len(restore.Status.ValidationErrors) > 0 {
d.Println()
d.Printf("Validation errors:")

View File

@@ -30,6 +30,8 @@ var (
{Name: "Name", Type: "string", Format: "name"},
{Name: "Backup"},
{Name: "Status"},
{Name: "Started"},
{Name: "Completed"},
{Name: "Errors"},
{Name: "Warnings"},
{Name: "Created"},
@@ -60,6 +62,8 @@ func printRestore(restore *v1.Restore) []metav1.TableRow {
restore.Name,
restore.Spec.BackupName,
status,
restore.Status.StartTimestamp,
restore.Status.CompletionTimestamp,
restore.Status.Errors,
restore.Status.Warnings,
restore.CreationTimestamp.Time,

View File

@@ -34,6 +34,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/cache"
@@ -90,6 +91,7 @@ type restoreController struct {
defaultBackupLocation string
metrics *metrics.ServerMetrics
logFormat logging.Format
clock clock.Clock
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
newBackupStore func(*velerov1api.BackupStorageLocation, persistence.ObjectStoreGetter, logrus.FieldLogger) (persistence.BackupStore, error)
@@ -125,6 +127,7 @@ func NewRestoreController(
defaultBackupLocation: defaultBackupLocation,
metrics: metrics,
logFormat: logFormat,
clock: &clock.RealClock{},
// use variables to refer to these functions so they can be
// replaced with fakes for testing.
@@ -235,6 +238,7 @@ func (c *restoreController) processRestore(restore *api.Restore) error {
restore.Status.Phase = api.RestorePhaseFailedValidation
c.metrics.RegisterRestoreValidationFailed(backupScheduleName)
} else {
restore.Status.StartTimestamp = &metav1.Time{Time: c.clock.Now()}
restore.Status.Phase = api.RestorePhaseInProgress
}
@@ -268,6 +272,7 @@ func (c *restoreController) processRestore(restore *api.Restore) error {
c.metrics.RegisterRestoreSuccess(backupScheduleName)
}
restore.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()}
c.logger.Debug("Updating restore's final status")
if _, err = patchRestore(original, restore, c.restoreClient); err != nil {
c.logger.WithError(errors.WithStack(err)).Info("Error updating restore's final status")

View File

@@ -32,6 +32,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/clock"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
@@ -232,6 +233,12 @@ func TestProcessQueueItem(t *testing.T) {
defaultStorageLocation := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Result()
now, err := time.Parse(time.RFC1123Z, time.RFC1123Z)
require.NoError(t, err)
now = now.Local()
timestamp := metav1.NewTime(now)
assert.NotNil(t, timestamp)
tests := []struct {
name string
restoreKey string
@@ -241,6 +248,8 @@ func TestProcessQueueItem(t *testing.T) {
restorerError error
expectedErr bool
expectedPhase string
expectedStartTime *metav1.Time
expectedCompletedTime *metav1.Time
expectedValidationErrors []string
expectedRestoreErrors int
expectedRestorerCall *velerov1api.Restore
@@ -282,13 +291,15 @@ func TestProcessQueueItem(t *testing.T) {
expectedValidationErrors: []string{"Either a backup or schedule must be specified as a source for the restore, but not both"},
},
{
name: "valid restore with schedule name gets executed",
location: defaultStorageLocation,
restore: NewRestore("foo", "bar", "", "ns-1", "", velerov1api.RestorePhaseNew).Schedule("sched-1").Result(),
backup: defaultBackup().StorageLocation("default").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "sched-1")).Phase(velerov1api.BackupPhaseCompleted).Result(),
expectedErr: false,
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Schedule("sched-1").Result(),
name: "valid restore with schedule name gets executed",
location: defaultStorageLocation,
restore: NewRestore("foo", "bar", "", "ns-1", "", velerov1api.RestorePhaseNew).Schedule("sched-1").Result(),
backup: defaultBackup().StorageLocation("default").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "sched-1")).Phase(velerov1api.BackupPhaseCompleted).Result(),
expectedErr: false,
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedStartTime: &timestamp,
expectedCompletedTime: &timestamp,
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Schedule("sched-1").Result(),
},
{
name: "restore with non-existent backup name fails",
@@ -307,17 +318,21 @@ func TestProcessQueueItem(t *testing.T) {
expectedErr: false,
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedFinalPhase: string(velerov1api.RestorePhasePartiallyFailed),
expectedStartTime: &timestamp,
expectedCompletedTime: &timestamp,
expectedRestoreErrors: 1,
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(),
},
{
name: "valid restore gets executed",
location: defaultStorageLocation,
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
expectedErr: false,
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(),
name: "valid restore gets executed",
location: defaultStorageLocation,
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
expectedErr: false,
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedStartTime: &timestamp,
expectedCompletedTime: &timestamp,
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(),
},
{
name: "restoration of nodes is not supported",
@@ -385,6 +400,8 @@ func TestProcessQueueItem(t *testing.T) {
restore: NewRestore(velerov1api.DefaultNamespace, "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedFinalPhase: string(velerov1api.RestorePhaseFailed),
expectedStartTime: &timestamp,
expectedCompletedTime: &timestamp,
backupStoreGetBackupContentsErr: errors.New("Couldn't download backup"),
backup: defaultBackup().StorageLocation("default").Result(),
},
@@ -431,7 +448,7 @@ func TestProcessQueueItem(t *testing.T) {
c.newBackupStore = func(*velerov1api.BackupStorageLocation, persistence.ObjectStoreGetter, logrus.FieldLogger) (persistence.BackupStore, error) {
return backupStore, nil
}
c.clock = clock.NewFakeClock(now)
if test.location != nil {
require.NoError(t, fakeClient.Create(context.Background(), test.location))
}
@@ -555,9 +572,11 @@ func TestProcessQueueItem(t *testing.T) {
}
type StatusPatch struct {
Phase velerov1api.RestorePhase `json:"phase"`
ValidationErrors []string `json:"validationErrors"`
Errors int `json:"errors"`
Phase velerov1api.RestorePhase `json:"phase"`
ValidationErrors []string `json:"validationErrors"`
Errors int `json:"errors"`
StartTimestamp *metav1.Time `json:"startTimestamp"`
CompletionTimestamp *metav1.Time `json:"completionTimestamp"`
}
type Patch struct {
@@ -588,6 +607,10 @@ func TestProcessQueueItem(t *testing.T) {
}
}
if test.expectedStartTime != nil {
expected.Status.StartTimestamp = test.expectedStartTime
}
velerotest.ValidatePatch(t, actions[0], expected, decode)
// if we don't expect a restore, validate it wasn't called and exit the test
@@ -602,16 +625,18 @@ func TestProcessQueueItem(t *testing.T) {
expected = Patch{
Status: StatusPatch{
Phase: velerov1api.RestorePhaseCompleted,
Errors: test.expectedRestoreErrors,
Phase: velerov1api.RestorePhaseCompleted,
Errors: test.expectedRestoreErrors,
CompletionTimestamp: test.expectedCompletedTime,
},
}
// Override our default expectations if the case requires it
if test.expectedFinalPhase != "" {
expected = Patch{
Status: StatusPatch{
Phase: velerov1api.RestorePhase(test.expectedFinalPhase),
Errors: test.expectedRestoreErrors,
Phase: velerov1api.RestorePhase(test.expectedFinalPhase),
Errors: test.expectedRestoreErrors,
CompletionTimestamp: test.expectedCompletedTime,
},
}
}