Skip to content

Commit

Permalink
enable a schedule to be provided as the source for a restore
Browse files Browse the repository at this point in the history
- ScheduleName is added as an API field to the Restore object
- Restore controller validates that exactly one of BackupName
  or ScheduleName has been provided
- If ScheduleName is provided, Restore controller populates
  BackupName with the name of the most recent successful backup
  created from the schedule
- --from-schedule flag is added to `ark restore create` CLI cmd

Signed-off-by: Steve Kriss <[email protected]>
  • Loading branch information
skriss committed Jul 9, 2018
1 parent f349f85 commit 706ae07
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 52 deletions.
7 changes: 6 additions & 1 deletion docs/cli-reference/ark_create_restore.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Create a restore
Create a restore

```
ark create restore [RESTORE_NAME] --from-backup BACKUP_NAME [flags]
ark create restore [RESTORE_NAME] [--from-backup BACKUP_NAME | --from-schedule SCHEDULE_NAME] [flags]
```

### Examples
Expand All @@ -19,6 +19,10 @@ ark create restore [RESTORE_NAME] --from-backup BACKUP_NAME [flags]
# create a restore with a default name ("backup-1-<timestamp>") from backup "backup-1"
ark restore create --from-backup backup-1
# create a restore from the latest successful backup triggered by schedule "schedule-1"
ark restore create --from-schedule schedule-1
```

### Options
Expand All @@ -27,6 +31,7 @@ ark create restore [RESTORE_NAME] --from-backup BACKUP_NAME [flags]
--exclude-namespaces stringArray namespaces to exclude from the restore
--exclude-resources stringArray resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io
--from-backup string backup to restore from
--from-schedule string schedule to restore from
-h, --help help for restore
--include-cluster-resources optionalBool[=true] include cluster-scoped resources in the restore
--include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *)
Expand Down
7 changes: 6 additions & 1 deletion docs/cli-reference/ark_restore_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Create a restore
Create a restore

```
ark restore create [RESTORE_NAME] --from-backup BACKUP_NAME [flags]
ark restore create [RESTORE_NAME] [--from-backup BACKUP_NAME | --from-schedule SCHEDULE_NAME] [flags]
```

### Examples
Expand All @@ -19,6 +19,10 @@ ark restore create [RESTORE_NAME] --from-backup BACKUP_NAME [flags]
# create a restore with a default name ("backup-1-<timestamp>") from backup "backup-1"
ark restore create --from-backup backup-1
# create a restore from the latest successful backup triggered by schedule "schedule-1"
ark restore create --from-schedule schedule-1
```

### Options
Expand All @@ -27,6 +31,7 @@ ark restore create [RESTORE_NAME] --from-backup BACKUP_NAME [flags]
--exclude-namespaces stringArray namespaces to exclude from the restore
--exclude-resources stringArray resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io
--from-backup string backup to restore from
--from-schedule string schedule to restore from
-h, --help help for create
--include-cluster-resources optionalBool[=true] include cluster-scoped resources in the restore
--include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *)
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/ark/v1/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ type RestoreSpec struct {
// from.
BackupName string `json:"backupName"`

// ScheduleName is the unique name of the Ark schedule to restore
// from. If specified, and BackupName is empty, Ark will restore
// from the most recent successful backup created from this schedule.
ScheduleName string `json:"scheduleName,omitempty"`

// IncludedNamespaces is a slice of namespace names to include objects
// from. If empty, all namespaces are included.
IncludedNamespaces []string `json:"includedNamespaces"`
Expand Down
67 changes: 45 additions & 22 deletions pkg/cmd/cli/restore/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {
o := NewCreateOptions()

c := &cobra.Command{
Use: use + " [RESTORE_NAME] --from-backup BACKUP_NAME",
Use: use + " [RESTORE_NAME] [--from-backup BACKUP_NAME | --from-schedule SCHEDULE_NAME]",
Short: "Create a restore",
Example: ` # create a restore named "restore-1" from backup "backup-1"
ark restore create restore-1 --from-backup backup-1
# create a restore with a default name ("backup-1-<timestamp>") from backup "backup-1"
ark restore create --from-backup backup-1`,
ark restore create --from-backup backup-1
# create a restore from the latest successful backup triggered by schedule "schedule-1"
ark restore create --from-schedule schedule-1
`,
Args: cobra.MaximumNArgs(1),
Run: func(c *cobra.Command, args []string) {
cmd.CheckError(o.Complete(args, f))
Expand All @@ -62,6 +66,7 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {

type CreateOptions struct {
BackupName string
ScheduleName string
RestoreName string
RestoreVolumes flag.OptionalBool
Labels flag.Map
Expand All @@ -88,6 +93,7 @@ func NewCreateOptions() *CreateOptions {

func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.BackupName, "from-backup", "", "backup to restore from")
flags.StringVar(&o.ScheduleName, "from-schedule", "", "schedule to restore from")
flags.Var(&o.IncludeNamespaces, "include-namespaces", "namespaces to include in the restore (use '*' for all namespaces)")
flags.Var(&o.ExcludeNamespaces, "exclude-namespaces", "namespaces to exclude from the restore")
flags.Var(&o.NamespaceMappings, "namespace-mappings", "namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,...")
Expand All @@ -104,39 +110,55 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
f.NoOptDefVal = "true"
}

func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error {
if len(o.BackupName) == 0 {
return errors.New("--from-backup is required")
}

if err := output.ValidateFlags(c); err != nil {
return err
}
func (o *CreateOptions) Complete(args []string, f client.Factory) error {
if len(args) == 1 {
o.RestoreName = args[0]
} else {
sourceName := o.BackupName
if o.ScheduleName != "" {
sourceName = o.ScheduleName
}

if o.client == nil {
// This should never happen
return errors.New("Ark client is not set; unable to proceed")
o.RestoreName = fmt.Sprintf("%s-%s", sourceName, time.Now().Format("20060102150405"))
}

if _, err := o.client.ArkV1().Backups(f.Namespace()).Get(o.BackupName, metav1.GetOptions{}); err != nil {
client, err := f.Client()
if err != nil {
return err
}
o.client = client

return nil
}

func (o *CreateOptions) Complete(args []string, f client.Factory) error {
if len(args) == 1 {
o.RestoreName = args[0]
} else {
o.RestoreName = fmt.Sprintf("%s-%s", o.BackupName, time.Now().Format("20060102150405"))
func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error {
if o.BackupName != "" && o.ScheduleName != "" {
return errors.New("either a backup or schedule must be specified, but not both")
}

client, err := f.Client()
if err != nil {
if o.BackupName == "" && o.ScheduleName == "" {
return errors.New("either a backup or schedule must be specified, but not both")
}

if err := output.ValidateFlags(c); err != nil {
return err
}
o.client = client

if o.client == nil {
// This should never happen
return errors.New("Ark client is not set; unable to proceed")
}

switch {
case o.BackupName != "":
if _, err := o.client.ArkV1().Backups(f.Namespace()).Get(o.BackupName, metav1.GetOptions{}); err != nil {
return err
}
case o.ScheduleName != "":
if _, err := o.client.ArkV1().Schedules(f.Namespace()).Get(o.ScheduleName, metav1.GetOptions{}); err != nil {
return err
}
}

return nil
}
Expand All @@ -155,6 +177,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
},
Spec: api.RestoreSpec{
BackupName: o.BackupName,
ScheduleName: o.ScheduleName,
IncludedNamespaces: o.IncludeNamespaces,
ExcludedNamespaces: o.ExcludeNamespaces,
IncludedResources: o.IncludeResources,
Expand Down
117 changes: 95 additions & 22 deletions pkg/controller/restore_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io"
"io/ioutil"
"os"
"sort"
"sync"
"time"

Expand All @@ -32,6 +33,7 @@ import (
"github.com/sirupsen/logrus"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
Expand All @@ -45,6 +47,7 @@ import (
listers "github.com/heptio/ark/pkg/generated/listers/ark/v1"
"github.com/heptio/ark/pkg/plugin"
"github.com/heptio/ark/pkg/restore"
"github.com/heptio/ark/pkg/util/boolptr"
"github.com/heptio/ark/pkg/util/collections"
kubeutil "github.com/heptio/ark/pkg/util/kube"
)
Expand Down Expand Up @@ -252,15 +255,8 @@ func (controller *restoreController) processRestore(key string) error {
// don't modify items in the cache
restore = restore.DeepCopy()

excludedResources := sets.NewString(restore.Spec.ExcludedResources...)
for _, nonrestorable := range nonRestorableResources {
if !excludedResources.Has(nonrestorable) {
restore.Spec.ExcludedResources = append(restore.Spec.ExcludedResources, nonrestorable)
}
}

// validation
if restore.Status.ValidationErrors = controller.getValidationErrors(restore); len(restore.Status.ValidationErrors) > 0 {
// complete & validate restore
if restore.Status.ValidationErrors = controller.completeAndValidate(restore); len(restore.Status.ValidationErrors) > 0 {
restore.Status.Phase = api.RestorePhaseFailedValidation
} else {
restore.Status.Phase = api.RestorePhaseInProgress
Expand Down Expand Up @@ -304,37 +300,114 @@ func (controller *restoreController) processRestore(key string) error {
return nil
}

func (controller *restoreController) getValidationErrors(itm *api.Restore) []string {
var validationErrors []string

if itm.Spec.BackupName == "" {
validationErrors = append(validationErrors, "BackupName must be non-empty and correspond to the name of a backup in object storage.")
} else if _, err := controller.fetchBackup(controller.bucket, itm.Spec.BackupName); err != nil {
validationErrors = append(validationErrors, fmt.Sprintf("Error retrieving backup: %v", err))
func (controller *restoreController) completeAndValidate(restore *api.Restore) []string {
// add non-restorable resources to restore's excluded resources
excludedResources := sets.NewString(restore.Spec.ExcludedResources...)
for _, nonrestorable := range nonRestorableResources {
if !excludedResources.Has(nonrestorable) {
restore.Spec.ExcludedResources = append(restore.Spec.ExcludedResources, nonrestorable)
}
}

includedResources := sets.NewString(itm.Spec.IncludedResources...)
var validationErrors []string

// validate that included resources don't contain any non-restorable resources
includedResources := sets.NewString(restore.Spec.IncludedResources...)
for _, nonRestorableResource := range nonRestorableResources {
if includedResources.Has(nonRestorableResource) {
validationErrors = append(validationErrors, fmt.Sprintf("%v are non-restorable resources", nonRestorableResource))
}
}

for _, err := range collections.ValidateIncludesExcludes(itm.Spec.IncludedNamespaces, itm.Spec.ExcludedNamespaces) {
validationErrors = append(validationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))
// validate included/excluded resources
for _, err := range collections.ValidateIncludesExcludes(restore.Spec.IncludedResources, restore.Spec.ExcludedResources) {
validationErrors = append(validationErrors, fmt.Sprintf("Invalid included/excluded resource lists: %v", err))
}

for _, err := range collections.ValidateIncludesExcludes(itm.Spec.IncludedResources, itm.Spec.ExcludedResources) {
validationErrors = append(validationErrors, fmt.Sprintf("Invalid included/excluded resource lists: %v", err))
// validate included/excluded namespaces
for _, err := range collections.ValidateIncludesExcludes(restore.Spec.IncludedNamespaces, restore.Spec.ExcludedNamespaces) {
validationErrors = append(validationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))
}

if !controller.pvProviderExists && itm.Spec.RestorePVs != nil && *itm.Spec.RestorePVs {
// validate that PV provider exists if we're restoring PVs
if boolptr.IsSetToTrue(restore.Spec.RestorePVs) && !controller.pvProviderExists {
validationErrors = append(validationErrors, "Server is not configured for PV snapshot restores")
}

// validate that exactly one of BackupName and ScheduleName have been specified
if !backupXorScheduleProvided(restore) {
return append(validationErrors, "Either a backup or schedule must be specified as a source for the restore, but not both")
}

// if ScheduleName is specified, fill in BackupName with the most recent successful backup from
// the schedule
if restore.Spec.ScheduleName != "" {
selector := labels.SelectorFromSet(labels.Set(map[string]string{
"ark-schedule": restore.Spec.ScheduleName,
}))

backups, err := controller.backupLister.Backups(controller.namespace).List(selector)
if err != nil {
return append(validationErrors, "Unable to list backups for schedule")
}
if len(backups) == 0 {
return append(validationErrors, "No backups found for schedule")
}

if backup := mostRecentCompletedBackup(backups); backup != nil {
restore.Spec.BackupName = backup.Name
} else {
return append(validationErrors, "No completed backups found for schedule")
}
}

// validate that we can fetch the source backup
if _, err := controller.fetchBackup(controller.bucket, restore.Spec.BackupName); err != nil {
return append(validationErrors, fmt.Sprintf("Error retrieving backup: %v", err))
}

return validationErrors
}

// backupXorScheduleProvided returns true if exactly one of BackupName and
// ScheduleName are non-empty for the restore, or false otherwise.
func backupXorScheduleProvided(restore *api.Restore) bool {
if restore.Spec.BackupName != "" && restore.Spec.ScheduleName != "" {
return false
}

if restore.Spec.BackupName == "" && restore.Spec.ScheduleName == "" {
return false
}

return true
}

// mostRecentCompletedBackup returns the most recent backup that's
// completed from a list of backups. Since the backups are expected
// to be from a single schedule, "most recent" is defined as first
// when sorted in reverse alphabetical order by name.
func mostRecentCompletedBackup(backups []*api.Backup) *api.Backup {
sort.Slice(backups, func(i, j int) bool {
// Use '>' because we want descending sort.
// Using Name rather than CreationTimestamp because in the case of
// backups synced into a new cluster, the CreationTimestamp value is
// time of creation in the new cluster rather than time of backup.
// TODO would be useful to have a new API field in backup.status
// that captures the time of backup as a time value (particularly
// for non-scheduled backups).
return backups[i].Name > backups[j].Name
})

for _, backup := range backups {
if backup.Status.Phase == api.BackupPhaseCompleted {
return backup
}
}

return nil
}

func (controller *restoreController) fetchBackup(bucket, name string) (*api.Backup, error) {
backup, err := controller.backupLister.Backups(controller.namespace).Get(name)
if err == nil {
Expand Down
Loading

0 comments on commit 706ae07

Please sign in to comment.