diff --git a/changelogs/unreleased/1892-skriss b/changelogs/unreleased/1892-skriss new file mode 100644 index 000000000..4c42d1c6b --- /dev/null +++ b/changelogs/unreleased/1892-skriss @@ -0,0 +1 @@ +backup sync controller: stop using `metadata/revision` file, do a full diff of bucket contents vs. cluster contents each sync interval diff --git a/pkg/apis/velero/v1/backup_storage_location.go b/pkg/apis/velero/v1/backup_storage_location.go index a11853e89..295eaed63 100644 --- a/pkg/apis/velero/v1/backup_storage_location.go +++ b/pkg/apis/velero/v1/backup_storage_location.go @@ -107,19 +107,28 @@ const ( ) // TODO(2.0): remove the AccessMode field from BackupStorageLocationStatus. +// TODO(2.0): remove the LastSyncedRevision field from BackupStorageLocationStatus. // BackupStorageLocationStatus describes the current status of a Velero BackupStorageLocation. type BackupStorageLocationStatus struct { + // Phase is the current state of the BackupStorageLocation. // +optional Phase BackupStorageLocationPhase `json:"phase,omitempty"` - // +optional - LastSyncedRevision types.UID `json:"lastSyncedRevision,omitempty"` - + // LastSyncedTime is the last time the contents of the location were synced into + // the cluster. // +optional // +nullable LastSyncedTime metav1.Time `json:"lastSyncedTime,omitempty"` + // LastSyncedRevision is the value of the `metadata/revision` file in the backup + // storage location the last time the BSL's contents were synced into the cluster. + // + // Deprecated: this field is no longer updated or used for detecting changes to + // the location's contents and will be removed entirely in v2.0. + // +optional + LastSyncedRevision types.UID `json:"lastSyncedRevision,omitempty"` + // AccessMode is an unused field. // // Deprecated: there is now an AccessMode field on the Spec and this field diff --git a/pkg/controller/backup_sync_controller.go b/pkg/controller/backup_sync_controller.go index b67cc7d00..0b48a3f56 100644 --- a/pkg/controller/backup_sync_controller.go +++ b/pkg/controller/backup_sync_controller.go @@ -23,7 +23,6 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" kuberrs "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" @@ -66,10 +65,10 @@ func NewBackupSyncController( newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, logger logrus.FieldLogger, ) Interface { - if syncPeriod < time.Minute { - logger.Infof("Provided backup sync period %v is too short. Setting to 1 minute", syncPeriod) + if syncPeriod <= 0 { syncPeriod = time.Minute } + logger.Infof("Backup sync period is %v", syncPeriod) c := &backupSyncController{ genericController: newGenericController("backup-sync", logger), @@ -98,33 +97,6 @@ func NewBackupSyncController( return c } -func shouldSync(location *velerov1api.BackupStorageLocation, now time.Time, backupStore persistence.BackupStore, log logrus.FieldLogger) (bool, string) { - log = log.WithFields(map[string]interface{}{ - "lastSyncedRevision": location.Status.LastSyncedRevision, - "lastSyncedTime": location.Status.LastSyncedTime.Time.Format(time.RFC1123Z), - }) - - revision, err := backupStore.GetRevision() - if err != nil { - log.WithError(err).Debugf("Unable to get backup store's revision file, syncing (this is not an error if a v0.10+ backup has not yet been taken into this location)") - return true, "" - } - log = log.WithField("revision", revision) - - if location.Status.LastSyncedTime.Add(time.Hour).Before(now) { - log.Debugf("Backup location hasn't been synced in more than %s, syncing", time.Hour) - return true, revision - } - - if string(location.Status.LastSyncedRevision) != revision { - log.Debugf("Backup location hasn't been synced since its last modification, syncing") - return true, revision - } - - log.Debugf("Backup location's contents haven't changed since last sync, not syncing") - return false, "" -} - // orderedBackupLocations returns a new slice with the default backup location first (if it exists), // followed by the rest of the locations in no particular order. func orderedBackupLocations(locations []*velerov1api.BackupStorageLocation, defaultLocationName string) []*velerov1api.BackupStorageLocation { @@ -162,6 +134,7 @@ func (c *backupSyncController) run() { for _, location := range locations { log := c.logger.WithField("backupLocation", location.Name) + log.Debug("Checking backup location for backups to sync into cluster") backupStore, err := c.newBackupStore(location, pluginManager, log) if err != nil { @@ -169,37 +142,42 @@ func (c *backupSyncController) run() { continue } - ok, revision := shouldSync(location, time.Now().UTC(), backupStore, log) - if !ok { - continue - } - log.Info("Syncing contents of backup store into cluster") - + // get a list of all the backups that are stored in the backup storage location res, err := backupStore.ListBackups() if err != nil { log.WithError(err).Error("Error listing backups in backup store") continue } backupStoreBackups := sets.NewString(res...) - log.WithField("backupCount", len(backupStoreBackups)).Info("Got backups from backup store") + log.WithField("backupCount", len(backupStoreBackups)).Debug("Got backups from backup store") - for backupName := range backupStoreBackups { + // get a list of all the backups that exist as custom resources in the cluster + clusterBackups, err := c.backupLister.Backups(c.namespace).List(labels.Everything()) + if err != nil { + log.WithError(errors.WithStack(err)).Error("Error getting backups from cluster, proceeding with sync into cluster") + } else { + log.WithField("backupCount", len(clusterBackups)).Debug("Got backups from cluster") + } + + // get a list of backups that *are* in the backup storage location and *aren't* in the cluster + clusterBackupsSet := sets.NewString() + for _, b := range clusterBackups { + clusterBackupsSet.Insert(b.Name) + } + backupsToSync := backupStoreBackups.Difference(clusterBackupsSet) + + if count := backupsToSync.Len(); count > 0 { + log.Infof("Found %v backups in the backup location that do not exist in the cluster and need to be synced", count) + } else { + log.Debug("No backups found in the backup location that need to be synced into the cluster") + } + + // sync each backup + for backupName := range backupsToSync { log = log.WithField("backup", backupName) - log.Debug("Checking this backup to see if it needs to be synced into the cluster") + log.Info("Attempting to sync backup into cluster") - // use the controller's namespace when getting the backup because that's where we - // are syncing backups to, regardless of the namespace of the cloud backup. - backup, err := c.backupClient.Backups(c.namespace).Get(backupName, metav1.GetOptions{}) - if err == nil { - log.Debug("Backup already exists in cluster") - continue - } - - if !kuberrs.IsNotFound(err) { - log.WithError(errors.WithStack(err)).Error("Error getting backup from client, proceeding with sync into cluster") - } - - backup, err = backupStore.GetBackupMetadata(backupName) + backup, err := backupStore.GetBackupMetadata(backupName) if err != nil { log.WithError(errors.WithStack(err)).Error("Error getting backup metadata from backup store") continue @@ -216,7 +194,8 @@ func (c *backupSyncController) run() { backup.Labels = make(map[string]string) } backup.Labels[velerov1api.StorageLocationLabel] = label.GetValidName(backup.Spec.StorageLocation) - // process the regular velero backup + + // attempt to create backup custom resource via API backup, err = c.backupClient.Backups(backup.Namespace).Create(backup) switch { case err != nil && kuberrs.IsAlreadyExists(err): @@ -226,7 +205,7 @@ func (c *backupSyncController) run() { log.WithError(errors.WithStack(err)).Error("Error syncing backup into cluster") continue default: - log.Debug("Synced backup into cluster") + log.Info("Successfully synced backup into cluster") } // process the pod volume backups from object store, if any @@ -237,7 +216,7 @@ func (c *backupSyncController) run() { } for _, podVolumeBackup := range podVolumeBackups { - log = log.WithField("podVolumeBackup", podVolumeBackup.Name) + log := log.WithField("podVolumeBackup", podVolumeBackup.Name) log.Debug("Checking this pod volume backup to see if it needs to be synced into the cluster") for i, ownerRef := range podVolumeBackup.OwnerReferences { @@ -270,11 +249,10 @@ func (c *backupSyncController) run() { c.deleteOrphanedBackups(location.Name, backupStoreBackups, log) - // update the location's status's last-synced fields + // update the location's last-synced time field patch := map[string]interface{}{ "status": map[string]interface{}{ - "lastSyncedTime": time.Now().UTC(), - "lastSyncedRevision": revision, + "lastSyncedTime": time.Now().UTC(), }, } @@ -289,31 +267,12 @@ func (c *backupSyncController) run() { types.MergePatchType, patchBytes, ); err != nil { - log.WithError(errors.WithStack(err)).Error("Error patching backup location's last-synced time and revision") + log.WithError(errors.WithStack(err)).Error("Error patching backup location's last-synced time") continue } } } -func patchStorageLocation(backup *velerov1api.Backup, client velerov1client.BackupInterface, location string) error { - patch := map[string]interface{}{ - "spec": map[string]interface{}{ - "storageLocation": location, - }, - } - - patchBytes, err := json.Marshal(patch) - if err != nil { - return errors.WithStack(err) - } - - if _, err := client.Patch(backup.Name, types.MergePatchType, patchBytes); err != nil { - return errors.WithStack(err) - } - - return nil -} - // deleteOrphanedBackups deletes backup objects (CRDs) from Kubernetes that have the specified location // and a phase of Completed, but no corresponding backup in object storage. func (c *backupSyncController) deleteOrphanedBackups(locationName string, backupStoreBackups sets.String, log logrus.FieldLogger) { diff --git a/pkg/controller/backup_sync_controller_test.go b/pkg/controller/backup_sync_controller_test.go index e1bc9e066..1cba1d533 100644 --- a/pkg/controller/backup_sync_controller_test.go +++ b/pkg/controller/backup_sync_controller_test.go @@ -20,13 +20,10 @@ import ( "testing" "time" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" core "k8s.io/client-go/testing" @@ -370,8 +367,6 @@ func TestBackupSyncControllerRun(t *testing.T) { backupStore, ok := backupStores[location.Name] require.True(t, ok, "no mock backup store for location %s", location.Name) - backupStore.On("GetRevision").Return("foo", nil) - var backupNames []string for _, bucket := range test.cloudBuckets[location.Spec.ObjectStorage.Bucket] { backupNames = append(backupNames, bucket.backup.Name) @@ -706,95 +701,6 @@ func TestStorageLabelsInDeleteOrphanedBackups(t *testing.T) { } } -func TestShouldSync(t *testing.T) { - c := clock.NewFakeClock(time.Now()) - - tests := []struct { - name string - location *velerov1api.BackupStorageLocation - backupStoreRevision string - now time.Time - expectSync bool - expectedRevision string - }{ - { - name: "BSL with no last-synced metadata should sync", - location: &velerov1api.BackupStorageLocation{}, - backupStoreRevision: "foo", - now: c.Now(), - expectSync: true, - expectedRevision: "foo", - }, - { - name: "BSL with unchanged revision last synced more than an hour ago should sync", - location: &velerov1api.BackupStorageLocation{ - Status: velerov1api.BackupStorageLocationStatus{ - LastSyncedRevision: types.UID("foo"), - LastSyncedTime: metav1.Time{Time: c.Now().Add(-61 * time.Minute)}, - }, - }, - backupStoreRevision: "foo", - now: c.Now(), - expectSync: true, - expectedRevision: "foo", - }, - { - name: "BSL with unchanged revision last synced less than an hour ago should not sync", - location: &velerov1api.BackupStorageLocation{ - Status: velerov1api.BackupStorageLocationStatus{ - LastSyncedRevision: types.UID("foo"), - LastSyncedTime: metav1.Time{Time: c.Now().Add(-59 * time.Minute)}, - }, - }, - backupStoreRevision: "foo", - now: c.Now(), - expectSync: false, - }, - { - name: "BSL with different revision than backup store last synced less than an hour ago should sync", - location: &velerov1api.BackupStorageLocation{ - Status: velerov1api.BackupStorageLocationStatus{ - LastSyncedRevision: types.UID("foo"), - LastSyncedTime: metav1.Time{Time: c.Now().Add(-time.Minute)}, - }, - }, - backupStoreRevision: "bar", - now: c.Now(), - expectSync: true, - expectedRevision: "bar", - }, - { - name: "BSL with different revision than backup store last synced more than an hour ago should sync", - location: &velerov1api.BackupStorageLocation{ - Status: velerov1api.BackupStorageLocationStatus{ - LastSyncedRevision: types.UID("foo"), - LastSyncedTime: metav1.Time{Time: c.Now().Add(-61 * time.Minute)}, - }, - }, - backupStoreRevision: "bar", - now: c.Now(), - expectSync: true, - expectedRevision: "bar", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - backupStore := new(persistencemocks.BackupStore) - if test.backupStoreRevision != "" { - backupStore.On("GetRevision").Return(test.backupStoreRevision, nil) - } else { - backupStore.On("GetRevision").Return("", errors.New("object revision not found")) - } - - shouldSync, rev := shouldSync(test.location, test.now, backupStore, velerotest.NewLogger()) - assert.Equal(t, test.expectSync, shouldSync) - assert.Equal(t, test.expectedRevision, rev) - }) - } - -} - func getDeleteActions(actions []core.Action) []core.Action { var deleteActions []core.Action for _, action := range actions { diff --git a/pkg/generated/crds/crds.go b/pkg/generated/crds/crds.go index fd1fda168..98c9fc39c 100644 --- a/pkg/generated/crds/crds.go +++ b/pkg/generated/crds/crds.go @@ -30,7 +30,7 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec\x1c\xcbn#\xb9\xf1\xee\xaf(8\x87\xd9\x05,\x0f\x06\xb9\x04\xbe\xcdz\x1cD\xd8ɬ\xb16\x9cC\x90\x03\xd5]\x92\x18\xb3\xc9\x0eɖ\xad\x04\xf9\xf7\xa0\xf8\xe8\xf7\x83\x9aUv\xb0X\xf3dSd\xb1\xde,\x16\x8b}\xb1Z\xad.XɟP\x1b\xae\xe4\r\xb0\x92\xe3\xabEI\xff\x99\xeb\xe7?\x99k\xae\xde\x1f>lв\x0f\x17\xcf\\\xe67p[\x19\xab\x8a\x9fѨJg\xf8\t\xb7\\r˕\xbc(в\x9cYvs\x01\x90id\xd4\xf9\xc8\v4\x96\x15\xe5\r\xc8J\x88\v\x00\xc9\n\xbc\x81\r˞\xab\xd2\\\x1fP\xa0V\xd7\\]\x98\x123\x9a\xb9Ӫ*o\xa0\xf9\xc1O1\xf4\x1b\x80G\xe1\a7\xdbu\bn쏭\xce\xcf\xdcX\xf7C)*\xcdD\xbd\x92\xeb3\\\xee*\xc1t\xec\xbd\x000\x99*\xf1\x06.//\x00\x0eL\xf0ܡ\xed\x17S%ʏ\xf7\xeb\xa7?>d{,\x98\xef\x04\xc8\xd1d\x9a\x97n\\X\x15\xb8\x01\x06O\x0egЁ5`\xf7\xcc\xd2\u007f\xa5F\x83\xd2\x1a\xb0{\x84\x8c\x95\xb6\xd2\bj\v?V\x1b\xd4\x12-\x9a\x00\x19 \x13\x95\xb1\xa8\xc1Xf\x11\x98\x05\x06\xa5\xe2\xd2\x02\x97`y\x81\xf0\xdd\xc7\xfb5\xa8\xcd?1\xb3\x06\x98́\x19\xa32\xce,\xe6pP\xa2*\xd0\xcf\xfd\xfe:\xc0,\xb5*Q[\x1e9H\xad%\xf1\xba\xafG\xd7;\"\u070f\x81\x9cd\x8c\x1e\xfd\x83\xef\xc3\x1c\x8cc\n\xd1a\xf7܀\xc6@\xa6c`\v,\xd0\x10&\x03\xd2\xd7\xf0\x80\x9a\x80\x80٫J\xe4\x90)y@M|\xca\xd4N\xf2\u007fא\rX\xe5\x96\x14\xccb\x10jl\\ZԒ\t\x12Y\x85W\x8e\x11\x05;\x82FZ\x03*ق憘k\xf8\xab\xd2\b\\n\xd5\r\xec\xad-\xcd\xcd\xfb\xf7;n\xa3\x8eg\xaa(*\xc9\xed\xf1}\xa6\xa4\xd5|SY\xa5\xcd\xfb\x1c\x0f(\u07b3\x92\xaf\x1c\x9e\xd2:\xbb(\xf2?D!\x9bw-\xc4\xec\x91t\xc9X\xcd\xe5\xae\xeev*;\xc9f\xd2]\xaf=~\x9aG\xb7\xe1&u\x11\x13~\xbe{xlk\x167]\x16;\xe66\xd3L\xc3g\xe2\v\x97[\xd4^N[\xad\n\a\x11e\xeeU\xcbi\xa5\xe0(\xbb<6զ\xe0\x96\x04\xfb\xaf\n\ri\xaf\xba\x86[&\xa5\xb2\xb0A\xa8ʜ\x94\xee\x1a\xd6\x12nY\x81\xe2\x96\x19<7\x97\x89\xa1fE\x1c\\\xe6s\xdb\xfdt\az\xe6\xd4\xdd\xd1ӌ\n\xc4\xdb\xf3C\x89YG\xedi\x0e\xdf\xf2\xcc)7l\x95n\xccݻ\x92\xeb\x16\xc01\x93\xa3\x86\xaf\x99\xa8r̿\x90?+Y\xd6\xff\xbd\x87\xca\xdd`8\x19\x8be\\\x92\xba\x90\xe3#˒ͯ\xce\xdf0\x8d=\xa0\x00$2.=4\xe7I\xf68\x82\xb63,\x8b\xc5\x00\xab\t\x86\aؕ\x10l#\xf0\x06\xac\xae\xfaK\xfbyLkv\x1c\xe5D\xdcE\xd2\x18Q\x8f\x0e\x06#x\xe6\xfchm\x16\x8e\x17\xbf!6\xec\x95z\x9e'\xfd/4\xa21k\xc8\xdc\xe6\v\x1bܳ\x03W:\x10\x1b\\\xe9\x06\x01_1\xab,\xe6\x03ԙ\x85\x9co\xb7\xa8\tJ\xb9g\x06\x8dw\xddS,\x98RbjzJl\x03\xfc\x1b\x911\x8d\x9e\xde)\x94\xe1e\x8f\xd2!3\xe4\xaeo\xb4\xcfʜ\x1fx^1\x01\\\x1a\xcbd\xe6\xe9`5N}:`Z\x9c\x03l\xbd\xf1G\x9c\x89\xf7\x1dG\xa0$\x82\xd2P\x90\x8b\x1b\x0e5\xa3\xf0a\x92\xdc\r3\x98\x83\xf2j\xa8+\x81&,\x94;\xff\xd2\xd8\xf5\xd5\x04\xe0Z\n~\xff\x13l\x83\x02\f\n̬\xd2cl\x98\x17\xaao\xcb>j\x82w#\xde*8\xcd\xe0BێJM\xc2\x04x\xd9\xf3l\xef\xf7*\xd2\x17\a\x05r\x85\xc6\xd9/+Kq\x1c'\x0e\xe6%\xedی\t7m֘\xfb\xb0\x86fݴE?״\x05\x8f\xd7\xe5e-\xfa\xdf\x0f+\xa3\xe3>Y1׃\x89\xe7TLb\"\xa7\xf0r\xbd\x05,J{\xbc\x02nc/E\xb0̝z&\xd9S\xaf\xfd\x9b\x13ĩ:\xbd\xee\xcf;\xa3N\xffB)\xd4K\xfff\x84\xe0\x9c\xfdC\xf0\xf5\x89\x02\xf8ܞs\x05|[\v \xbf\x82-\x17\x16uO\x12s\xe4\xaayI\xfcR\x16,\xefT\xd4\nf\xb3\xfd\xdd+EG.c17\xb6Ǎ\xfeT\x1fSƨ\xba\xbb\x99\xceB\x05wB\xe2\x1a\v\u007f\xeezt\x1clz\\\xe4\xf3\xf1\xcb'̧\x99\x02)\x1a6 \xe1c\x0f\xcd\xf6\xb2!DN# \x04)\xf5\xe9\xc2\x1f\x99\xaf\x80\xc13\x1e}tA\a\xf8\x125\xa3eh\xf0\"D\x8d\xee\xdc\xee\x14\xea\x19\x8f\x0eH8\x8a/\xccM\x13\xbdo\xcfx\\\x1e\xd4c\x1ba\xc3MH-\x10\xff\xa8\xc31\xc0\x9d\xf4RY\x06.\x91\x12=\xcc\x12Q\x90\xea\"b\x8b\xdc>\x99\xbcZLM2\xc0\v\xf2\x9d\xf1B!m\xdf\xf32\x89@r\x9d`\xd0\xd9DL\xa4<1\xc1\xf3z\x19\xaf\xdfky\x05_\x94]˩`\xb5\xdb\xee^\xb9\t\xf9\xabO\n\xcd\x17e]\xcfٙ\xe8Q>\x99\x85~\x9a3!\xe9\xdd0\xd1\xdfN\xd0,*\xb1ok\u007fªE\xc2\r\xac%\x9d!<\xaf|F\xcd/6\xe7\xed\xbb\xad\xa8\x8c\xcb\xc0H%Wn\xb3\xbb\x1e['\xb08Q\x91\xdbR\x18\xa2U/\xe9\x97K\x82\xf8H\xfb\x82\x9f\xed\xb3\x83\x82e\x98C^9&\xbat\x17\xb3\xb8\xe3\x19\x14\xa8w\xd3\x1bA\xbb\x95\xe4\xb3S\x96O\U000a5f9d\xa4O)[sl\xc1\x19\xe7Kh\xac\xc86\x17\xc7D\xd1.\f\x1cMxM\x0f\\\xa2\xc3m\x92.nX\xe0&\xcbsw\t\xc0\xc4}\xb2\xf7N\xe6\xfcp\xdf\xf6(\xf9=\xae`%Y\xe7\u007fh\xabrJ\xfb_(\x19\u05cb\x16\xfa\xd1]\a\b\xec\xcc\fY\xa1\xf6\"\x04\x9f\x1b i\x1e\x98\xe8gIG\xc8R\xe45P\xf8mXm\a\x91\xc6\x15\xbc\xec\x95\xf1\xbb▣ȁ\xcfEZ\xd4.\x9f\xf1xy5\xb0\xf1˵\xbc\xf4\xdb\xf3\xc0b\xe3^\xbe\x00XIq\x84K7\xf3\xf2\xebC\x97$\xadK\x18\xe4\xee\x86҂Y:\xcd\xc5]\x9c\xa6\xd5\xf7\x10\x14\x8aNc\x9b\xa0s\xa526\x11\x89{e\xac\xcf\xd0u\x82Ǒ\xdc\xd0\xfc\x99&䄀m\xfdݏ\xd21\xedO\x8e\xac\x97\xaa$)\x19\x1cMp\x0e \xe6\x01$\x13\x02.\x1b\x1b\xf5\xfe\xf1\xd2\xdf\x05\xb8%X\xe6\u0082\x19\x88\xa4\n\xa5V\x19\x1a3\xa7\x0e\x8b\x9ew!\xe1V'ۘ?T\xf8T\xfb\\r/\xb6\u0530\x91XsR\x98}\xf7\xda\xca\x01\x92i\xd3\xff\xf3jv\x1aF\xd42U\x14L.n\x16\x03\xe4n\xfd\xbch\n\x01\x8c\x0f\xd9\xf5\xaerf\x9c\x1a\xe9\x05\xa5\xf9\xb6\x1bl\xc1\xe5\xda\x01\x87\x0fgݎ!\xbaD<=\xa4\xbe\x8d3\x1b6\xd7\x1d\xde6K5L\xb9\x8f\xb5\x97=j\xecHj\x98\x19v\xe1\x9cT\xb6u\x8a|\xe8\x8e\x1d9\\P\x8cÞ\x112\xa1\xaa\xbc\x86=$\xc5=C<\xc2\xfd\x93s\xf2\xee)L\xd6<\x04\n\xae<\x06?\xfdwB?\x9c\xf3\xb0a\xac\xd2l\x87\x9fU\xd6zm:E\u007fwl\xe7\xad`\x10j<\xd2\xc7:\b\x16_\xb1u\xa7\x8eŎ!\xcb\xe6\xe3\xc3\xd6\xe9\x8b0\x1c\xca{\xd2\xf2\xac\x15\xb3D<>~\xf6\x88[^\xe0\xf5\xa7\xca\x1f\xe4V%\xd3\x06\x89\u007f\x91 ?iC\u007f\xee\xd5\xcb\x00a\xa1\x02\xa5?\xf4\xf1\xd5\xe8rx\ued18\x8c\xb5\u007f/\x1b\x15,\xb2i^\x1d\x9f\xc6\xe7\xb4bіP\xfc\xc1Fm\xa7f\r\bl=\xe6\xa5h\xdfW\xb4\x9c\xeb\x89ڸs\x1e\u007f\"i\x99\xad\xcc\xd2#I7(>h\x0e\x19\xdfJ\xbb\x17f\x1e\x80WƓ\xdfI\x86\xf4V\xe7\xf9\xf8\x9cLn\x87\xe3\xddsb\x9d{\xa4\\Z\x8dE%\u007fa\xa6N\xa0\x8dx\xb4\x06\x98\x9f\xe7\x82\x01\x82\x859\xe0\x01%(\xe9\xf2e\xee\x05\x97\u007f\xc4ޟ3<\xbf\xb6`\x84t\\U\n\xc5\xf2h\xb9\x01\xb5\xf8D\xfa\xd1\xf9#}@\xfd\xceLB\xacLH\x8e\x8c\x90\xdf\u05ec\xad\xd2\x05\xb37\x903\x8b\xab\x11\x80\t~lD\xa5\\\xf2x\xe1\xe9\xa6\x1b\xe2\xad\xc3坝J\b\x11\x12\xcf\x05\x1a\xc3v\xf1\xcd\xe6\v\xb9\xa3\x1dJ\xda\xe4F\xb2D!\x14k\x12\x97\xdd\xf7\x8b\xfeD\xc72K\xe7_\x8fZ8¶F\xbd\x1bڜP;:a\xbb\x81\xe1\x19u\xf0\xcf㎄K\x8b;\xec\x86G\xf8Zr\xbd\xec\xcb\xef\xeaa\xc4\x11wtw\x16\xde|D\x00\x05\xdfqr\x88$\xd8\x1d\xd3\x1b\xb6\xc3U\xa6\x04\x9d\xa3\xb8\x92}\x8c\xfe?ru\xcfCg\t\xb9\xa7\x11\xf5-O\xcb\xe61\ne|\xbf\x1c\xbb\rX\xc1\x17\xec\xbbz\x9f\xe0\xc7\xfc\xa9\xfe\"\xc3`\xc0Z\xdek\xb5\xa3\xc8i\xf0\xd3m\xb4\xee\xc1/\xf7L[΄8z\xf0\x13\xab\x0e\xba?!\xd9ׄC\x1da\xa0\xb1L\xdb4\xdf\xf5\xd0\x19\xba\xe0\xb6\x1c\\̯\xe1\x01KFF2PfwU}\xdb\xff\xf8\xc6\x15\x85\xb6\xf1\x83\x14\xfe\x03\x03ٞɝ{\xd3\n\x1a\xddn\uf7ee\f v\xfcP\xc7\xeftQ\xffu\\N\U000c93bbe\xe7\xf3\xd4\x1b\xdc\xcb?\x92\x1bj\xe0E\x97\xf1\x1d\xdf\x0e\xb7\xe5\xb2\x14<#l\xbf\xffFy\xc5\xc3\xf0s\x1dCr\xc3\xe7:\x82Y\x06\xbd\xf1r\x88\x00\xd2]Z782\x1f\xad\xa5\x83\xf2\xf0@9\x13\x1d5\x93\"NVY&@V\xc5\x06\xb5\x13A\x1c0``\xfc\x96I\x04\x15\xee\xc0&ádBj\xe7p\n!\xf5\xa4)BL\x95eh̶\x12bx\U000ad8cd3R\xf5\xc24\x85\x98\xf3\x06\xf0\xb70hd\xff\r\xf3ϻ\x03\xb76\xe0\x88߯\xb4\x05\x8fD\xb1\xbd\xaehAp\xf8\xd0\xfc\xe7ط\n\xdf$:\xf8J\n\xe7\xf0\xf2\x96u\x06TBO\x13\x1a\xb3,C\xd2\xdd/\xfd\xcf\x13\xb9\x8f\t5_ r\xfffJ\xfa\xfc\x86\xb9\x81\xbf\xff\xe3\x02\xc2\t\xeb)\xe2A\x9d\xff\v\x00\x00\xff\xffj\x12iȎI\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdr\xdb6\x10\xbe\xeb)v܃/\x11ݴ\x97\x0eo\x89݃\xa7I\xec\xb1bw:m\x0f\x10\xb0\x94P\x83\x00\x82\x05\xe8\xa8O\xdfY\x80\xd4\x0fE\xd9\xf1L[\u07b8X,>|\xfb\x8b\xd9|>\x9f\t\xaf\x1f0\x90v\xb6\x06\xe15~\x8dh\xf9\x8f\xaaǟ\xa8\xd2\xee\xa2{\xbb\xc4(\xde\xce\x1e\xb5U5\\&\x8a\xae\xbdCr)H\xbc\xc2F[\x1d\xb5\xb3\xb3\x16\xa3P\"\x8az\x06 \x03\n\x16~\xd6-R\x14\xad\xaf\xc1&cf\x00V\xb4X\xc3R\xc8\xc7\xe4)\xba Vh\x9c\xcc\xcaTuh0\xb8J\xbb\x19y\x94lh\x15\\\xf25\xec\x16\x8a\x05\xe25\x80\x82\xe8}6\xb6(\xc6>\xf4\xc6\xf2\xba\xd1\x14\u007f9\xad\xf3AS\xcczޤ \xcc)XY\x85\xb4]%#\xc2\t\xa5\x19\x00I籆\xb3\xb3\x19@'\x8cVy\xa1\x00u\x1e\xed\xbb\xdb\xeb\x87\x1f\x17r\x8d\xad(B\x00\x85$\x83\xf6Yo\x1a\"h\x02\x01\xc3)\xf0\xb4ƀ\xf0\x90\xd9\x00\x86\x80\xd4\xe3\xe9-\x02\xb8\xe5_(#U\xbd\xc0\a\xe71D=P\xc6ߞǷ\xb2\x11\x98sF[t@\xb1\x8f\x91 \xae\x11\xba\"C\x05\x94o\x02\xae\x81\xb8\xd6\x04\x01}@B\x1bw\xeco\x115 l\x8f\xab\x82\x05\x066\x02\xb4v\xc9(\x90\xcev\x18\"\x04\x94ne\xf5\xdf[\xcb\x04\xd1\xe5#\x8d\x88\xd8\xfbi\xf8\xb4\x8d\x18\xac0\xccs\xc27 \xac\x82Vl \x9f\x01\xc9\xeeY\xcb*T\xc1G\x17\x10\xb4m\\\r\xeb\x18=\xd5\x17\x17+\x1d\x87\x18\x97\xaem\x93\xd5qs!\x9d\x8dA/St\x81.\x14vh.\x84\xd7\xf3\x8cӖ0m\xd5w\xa1\x8f\u007f:\xdf\x03\x167\x1c\x00\x14\x83\xb6\xab\xad8\xc7\xe8I\x9a9:\x8b\x8f˶\x02w\xc7&\x8b\x98\x84\xbb\x9f\x17\x9fa843~Hq&w\xb7\x8dv<3/\xda6\x18\x8a\x9f\x9a\xe0\xdal\x11\xad\xf2Nۘ\u007f\xa4\xd1h\x0f9\xa6\xb4lud\xc7~IH\x91\xddQ\xc1\xa5\xb0\xd6EX\"$\xafDDU\xc1\xb5\x85KѢ\xb9\x14\x84\xff6\xcbL(͙\xc1\x97y\xde/?\x87\x8a\x85\x9c\xadx(-\x93\x0e\x99L\u0085Gy\x90\x05lB7\xbaO\xca\xc6\x05\x10}R\x1eP8i\xac\xdaS\x99JΜ\xa0R\"\xd1G\xa7\xf0P>\x02\xfbn\xabv\x80\xcech5\xe5\x12\x9e\xb1\xb1\xac\x14\t\xe8\xab\xd6\xc8(l\vL5ZA\x9b\xda1\x849ܡP7\xd6l&\x17~\r:\x8e\x0f\x98t\x18\u007f\xd2\xd9F\xaf\xc6'\b\xa5rK\x11\xe6\xf6\x04A\xcf\x1a\x1d\xb1t\x99\xcf\xe0$c2|p\x9dV\x18\xe6\x83\x0f{\f)\xf4\xce\xd4h\x14\x8dy\x98\f$\xd8&^\xef\xe2g\x9du\xb3\xaf\xb9-\xef=\x8a!\xae0r\xc6\x13Xdϊ0\xa6\x18\xb8(Jg-g{t \xb6\xf79\xa7\xa1\b\xf4>\x1e_\xe1T\xac\xf1\xb7L\xf2\x11\xe3\xb1|\x9c\x1cY\x8d\x99\xcc!U\xfe\xa2\x83D\x98\xb9}\x1e\xc0\v>c\x84\xd8\xe8\xaf/\xa2\xb8\xcdj\x03\n/\xe2\x1a\xb4%\xad\x10\xc4\x04\xa6\x89\xb4\x1c\xbe\x01'\xdc\xf8\x12m\xafD̕Q\aT\xc7\tR`|k\f\r.|6|n{\xa5\xed\xbd\x87\xff܀\xc7\t>\x1d\xbfG\xb7\x98\xba\xc1\xfc0\xa6\x0fV\x86C_,\xb1Q\xc4D\xaf,\xb2yO\xaf\xb8\xec\x13B\xa6\x10\xd0\xc6\xde \xb8\xe6\xb0L\x0e\x93\xd0\u007f]h\xcf\xf6*-7k\v\xc9&BU\xaaE\x05\u007fX\xb8\xe2\xd6+\xb9%\u058c\x9c\xbb \x1d\x85\x93uO\xbcy\xcfZ6\x00\xce\xe6\xdb\xe6>óL\xe9\xd4y\xe9I\x1b\xc3\xfd6`\xeb:TG&\xb9[\x064\x1b\x10L\x0ft?T\xdfWg\xffs\x157\x82\xe2bc%\xaa;\xec\xf4x\xae\x06\x83c\xf9\x04\xec\xdb\xc1l\x84\xae\xc6XYJ-\x1d|iw\xd3\x1d\x941\xe9JQ\xee\xf6\fA\xcf:\x9d\xb0t\x95\xf6\x90\"\x132\xea\x18Zk0.\xfb\x18f\fM\xcc\xc1\xb4\xe8\fMy\x98M$\x18\n/\x87\xf8\xd9`\xdd\x1c[\x0e\xed=\xa3\xe8\xf3\nY*\x9e\xc0\xa3DV\xc5)\xc5 MQ\a\xef\xa5\xda9\x80\x1a\xcesI}\x13\xc81\x9e\x1e\xe1\\\xaeɷm\xf4\x03\xf2\xa9|Z\x1c\xc9L\x98L)խ8@C\x98\xb8}\x1e\xc0\v1\x13\x84X\xda//\xa2\xb8Mf=\x8aZ\xf1\x1e\xac'k\x10\xd4\f\xa6\x99\xb2\xec\xbf\x1e'\xdc\xd4]\xb6}%b\xe9\x8c6\xa29-\x90\x0e\xc6ks\xa8\x0f\xe1\xb3\xe9s\x9b\x8d\x86s\xf7\xebt\x01O\v|>\u007fON1w\x82\xe58\xa7G\x9a~\xd3\x17[,+n\xe8+\x9bl\xfa'\x1bnsA\xe8&F\xf4\x9c\x1dB(\xc7m\xb2\x9f\x84\xfe\xedF{q\xd4i\xe5\xb2\xf6\xd0\xf8\x86\xd0tݢ\x80\xdf<\xbc\x97\xabW˕\xb8\x16\xe4r\v\xd2I:\xf9\xf0(?\x1fyK\x0e \xf8t\xdat\xcf\xc8,\xd3\xdd\xd4I\xf5h\x9d\x93\xfb6b\x15Z4'.嶌\xe8\x9e@\t=\xd0~[|S\\\xfc\xc7]\xdc)\xe2͓\xd7h\uec35ӹ\xf2\x94\xcd\xeb\x13\xfb>\xab\xbb\xe9'\xa7\xf4\xe7\xfeJ_\xc5l\xf6\xf9\xe4\xf8\xa5u2o̔\xc0ah\xee\x86Hb`[aZ\xbd\xdb\\_\x92\xf4Q\x96Y\xe9\xc4飄\x8f\x12@\x195C\x1e\x91\x1ab\x8c3\xc1\x1ebe\t|\x00\x17\xfcnT\"ݗ\a&\b\x11\xba\xd4\t\x11\f2\xea4\xe3\xe9\xbd\xf2;<̼\x19\xfb\x11JI\x8cS\xa4\xe3\xec8d\x83\xf5\xf3\xa9\xf0\x8a\x18ʛ\xed\xd9\xf8]\x8fL\xfbЍ\x19\x1eP\xe7X\xba\xe3g٫\xb9\x9eX\x97!V\x8a\xd7 D.e\xab\x89^\x9e\x98j\xebp\r\x1c\x9bWgo\xbdW\xf4\xfc\x81oŢ?\xe7qK\x1aR\xf5\xc5\x06\x04g\xcb\xf0m\xablB}\xa2\xf9٫3\xba3g\x99\xe9\xc5\x13Q\xdb?\xf4\xdb7\x87U\xea\x89\xcb\xfc2O\n\x00\x92׃9\"2WU\x96\x1c\x1a\xbctК\xd1\xfc4}\x95_\\\x8c\x9e\xd6i\xa9\x83\xef&;Zï\xbf/:\xafh\xee{\x1c\"\xfc;\x00\x00\xff\xff\xc2\xdb\xde:\x94\x10\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xac\x96Ao\xeb6\f\xc7\xef\xfe\x14\xc4\xdb\xe1]\x16\a\x0f\xbb\f\xbem}\xefPl+\x8a\xf6\xa1\x97a\aEbb\xad\xb6\xa4\x91T\xba\xec\xd3\x0f\x94\xe2$N]\xf4\r\x98o\xa2(\x8a\xfa\xfd)Z\xcdj\xb5jL\xf2OH\xecc\xe8\xc0$\x8f\u007f\v\x06\x1dq\xfb\xfc#\xb7>\xae\xf7\x9f6(\xe6S\xf3\xec\x83\xeb\xe0&\xb3\xc4\xf1\x019f\xb2\xf8\x19\xb7>x\xf114#\x8aqFL\xd7\x00XB\xa3Ư~D\x163\xa6\x0eB\x1e\x86\x06 \x98\x11;p8\xa0\xe0\xc6\xd8\xe7\x9c\b\xff\xca\xc8\xc2\xed\x1e\a\xa4\xd8\xfa\xd8pB\xabav\x14s\xea\xe0\x97P?\x97P\x0f5T\x99\x1d<\xcb/oy\xfc\xea\x8f^i\xc8d\x86儊\x03\xfb\xb0˃\xa1E\x97\x06\x80mL\xd8\xc1\x87\x0f\r\xc0\xde\fޕs\xd7\x04c\xc2\xf0\xd3\xfd\xed\xd3\x0f\x8f\xb6\xc7\xd1T#\x80C\xb6\xe4S\xf1[J\x0e<\x83\x81\xe3\x16 \xf1\xb83Ā\x10\t\xc6H\b5\rn\x8f!\x13ń$~B\xa3߅\xae'\xdb\xd5\xe6\x1f5\xbb\xea\x03N\x95D\x06\xe9\x11\xf6Ն\x0e\xb8d\x0eq\v\xd2{\x06\xc2D\xc8\x18\xa4\x9c\xf2\",\xa8\x8b\t\x107\u007f\xa2\x95\x16\x1e\x914\bp\x1f\xf3\xe0\xc0ưG\x12 \xb4q\x17\xfc?\xa7Ȭ\xe7\xd3-\a#\x93r\xd3\xe7\x83 \x053(\u05cc߃\t\x0eFs\x00B\xdd\x03r\xb8\x88V\\\xb8\x85\xdf\x14\x8e\x0f\xdb\xd8A/\x92\xb8[\xafw^\xa6J\xb6q\x1cs\xf0rX\xdb\x18\x84\xfc&K$^;\xdc\xe3\xb06ɯJ\x9eAJ\xf5\x8f\xee;:V9\u007f\xbcHL\x0e*8\v\xf9\xb0;\x99K-\xbe\x89Y밪Z\x97\xd5t\xcf4դ\x10\x1e\xbe<~\x85i\xd3B|\x8e\xb8\xc0=/\xe33g\xe5\xe2\xc3\x16\xa9괥8\x96\x88\x18\\\x8a>H\x19\xd8\xc1c\x983\xe6\xbc\x19\xbd\xf0Tm*G\v7&\x84(\xb0A\xc8\xc9\x19A\xd7\xc2m\x80\x1b3\xe2pc\x18\xffo\xca\n\x94WJ\xf0}ΗMf\xeeX\xe1\x9c\xccS\vY\x14d\xe1\xd2=&\xb4*\x91rҵ~\xebm)r\xd8F\x82\x97\xde\xdb~\xbat3\x80\xa7\xeb\xd9^\x98\x97\xae\xa3~5\xc0\x9d\xb6\xc0\x99\xfd\x8d\xc3B\x91\xc5\x13\xceJku\x11\xe6]\nb$\xf3\u007f\xe2PVL$l&\xc2 \xc78\xe5\x8e/-\xfa\x96\xb3#Q$\xbe>\xf7,\x9d/\xc5E\x9b\x85\x18\x1f\x18L8\x1c\x97\x81\xf4F\xe0\x05I+\xdaƬ\x9d\x01\x1d\xb8|\xc5눢\xc7*\x8aʗ(Zdn\xaf\xbc\xbc\xe0\xf8*\x9b7u\xd0O\u007f`f3`\aB\x19\x17\xf53D\xe60\x9bI\xbd\xe1Wb\xcf\x0e}\xaf\x1eK\xbc\xb1v]|\x0fx\x81\x1b\xf2x\xbd\xcb\n\xee\xf0\xe5\x95\xed6\xdcS\xdc\x112\xbf\x9a\xba\xaf\xa4\xd0}[m.\x14ܕi?=+\xf6\x9fΣ\x02}u|\a\x94\t\x00\xd6.\xe6.\xc0\xb2D2\xbb\t\xf5\xb9\x8a\x8d\xb5\x98\x04\xdd\xdd\xf5+\xa0\xfc\u007fϿ\xf32\xb41\xb8\xf24\xe1\x0e~\xff\xa3\xa9Q\xd1=My\xa8\xf1\xdf\x00\x00\x00\xff\xff\xe9\x15A\x19\x02\t\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xacVMo\xe46\f\xbd\xfbW\x10\xdbö\xc0ڃ\xa0\x97·6\xd9âi\x10L\xd2\\\x8a\x1e42\xc7VcK\xaaHM6\xfd\xf5\x05%{><\x9ed\v\xd47S\x14\xf5\xf4\xf8\x9e\xa4\xa2,\xcbBy\U000c404c\xb35(o\xf0+\xa3\x95?\xaa\x9e\u007f\xa2ʸ\xd5\xeej\x83\xac\xae\x8agc\x9b\x1a\xae#\xb1\x1b\xd6H.\x06\x8d7\xb85ְq\xb6\x18\x90U\xa3X\xd5\x05\x80\x0e\xa8$\xf8h\x06$V\x83\xaf\xc1ƾ/\x00\xac\x1a\xb0\x86ƽ\xd8ީ&\xe0\xdf\x11\x89\xa9\xdaa\x8f\xc1U\xc6\x15\xe4QK\x896\xb8\xe8k8\f\xe4\xb9$c\x00\x19\xcb\xcdXf\x9dˤ\x91\xde\x10\xff\xba4zk\xc6\f\xdfǠ\xfas\x10i\x90\x8cmc\xaf\xc2\xd9p\x01@\xday\xac\xe1Ç\x02`\xa7zӤ=f@Σ\xfd\xf9\xfe\xcbӏ\x0f\xba\xc3A\xe5 @\x83\xa4\x83\xf1)o\x0e\b\f\x81\x82\xb1<\xb0ۯ\bʂ\nl\xb6J3l\x83\x1b`\xa3\xf4s\xf4cM\x00\xb7\xf9\v5\x03\xb1\v\xaa\xc5O@Qw\xa0\xa4ZN\x84\u07b5\xb05=V\xe3\x14\x1f\x9c\xc7\xc0f\xa2O\xbe\xa3\xbe\xefc3\xc0\x1feG9\a\x1a\xe94\x12p\x87\xb0\xcb1l\x80\xd2n\xc1m\x81;C\x10\xd0\a$\xb4\x9c\x989*\v\x92\xa2숼\x82\a\fR\x04\xa8s\xb1o@;\xbb\xc3\xc0\x10P\xbb֚\u007f\xf6\x95Ix\x91%{\xc5S\x87\xa7\xcfX\xc6`U/\xbd\x88\xf8\t\x94m`P\xaf\x100\xb1\x13\xedQ\xb5\x94B\x15\xfc\xe6\x02\x82\xb1[WC\xc7\xec\xa9^\xadZÓҵ\x1b\x86h\r\xbf\xae\xb4\xb3\x1c\xcc&\xb2\v\xb4jp\x87\xfdJyS&\x9c\x96\x93;\x86\xe6\xbb0\xba\x80>\x1e\x01\xe3W\x11\tq0\xb6݇\x93^/\xd2,z\xcdj\xc8\xd32\xdc\x03\x9b\x12\x12\x12֟\x1f\x1eaZ41~Jq\x96\xc5~\x1a\x1dx\x16^\x8c\xddb\xc8}J\xa2\x92\x8ah\x1b\xef\x8c\xe5\xf4\xa3{\x83\xf6\x94c\x8a\x9b\xc10M*\x95vTp\xad\xacu\f\x1b\x84\xe8\x1b\xc5\xd8T\xf0\xc5µ\x1a\xb0\xbfV\x84\xff7\xcbB(\x95\xc2\xe0\xfb<\x1f\x1fB\xa7\x89\x99\x9c}x:f\x16\x1b23\xea\x83G-\xed\x11\x8ed\x9e\xd9\x1a\x9d\x04\x0e[\x17@\x1d|;\xb2T\x1d\xd5]r^\x02\xa5B\x8b|\x1a\x9b\xa1xL)\xb2\xf0K\xa7N\x0f\x88\xef\xb1j+q9\x8d\x10\xb2\xef\u007f\xa8f\xf5.\xad\xbe$\xc9E\f\x932e\xeb£\xd8X\x0e\x96c4\xf3E\xe5C\x1b\x87\xa5\xe2%\xfc\x92\x90\u07ba\xf6\x8d\xd1kgY\xf4\xfbFʓ\xeb\xe3\x80\x0fVy\xea\x1c\xbf\x918\xddT\xfb\xe3\u007f\x9e\xb6F9G\xf1\x12\xa2qx\x8d\x14\xfbED\x8b:\x9c\xbetݽG\xf2\x9d\x1ap\"Y&\xe4\xe3\x14\xe19n0Xd\xa4\x83\xe9_\fw\xf0\xd2\x19\xdd-T\x854-\xf5GN\x13\"\xa7M\xf2\xe7\u007f\x83-26\x01\xcf\xd4Q&͜\x05\x05r\xb1T|f\xb9\xe5\xc2\xe5h\x85w\rˊ#}\xb3eS\xf6D\xaa\x8e!\xa0\xe5\xb1F\xba\x8a\xe6\x13\xbeų\x93\xe0\u007f_\u07feiܛC\x9e\xdcl\xac\x8c\xcd8|\xc0\x92L+\x17\xa7\x8c\x89u\x93\xb1\xe6\x04\xe4\xef\xf8\x02\u007f\xb7k\xf8՛p\xf4\x1e\xb9\x00\xed\xf3>-\x9f+h\xf3\x850\u007f\x9a\xa4rH\xe9N\xd5ʞa\xdb 4\xd8#c\x03\x9b\xd7|0\xbe\x12\xe30ǻuaP\\\x83\\\x13%\x9b3\xa1ȫPmz\xac\x81C\\V\xd1\xc2f}\xa7\xe8\xccV'\xfb\xbc\x97\x8c\xa5\xf6\xef\xcd\xf5F\xff\xe1\xc2\x01V\xc2\x1d\xbe\x9c\xc5\xee\x83\xd3H\x84sc\\@\xbf \xeeYh7\xbd\xc6wW\x87\xbf$\xc5r|>\xa7\x01\x00\x92˽9\xa2n|\x10\x8e\x91\x83c\x94\xd6\xe8\x19\x9b\xbb\xf9\x03:=e\x0f/\xe2\xf4\xab\x9dmҋ\x9ej\xf8\xe3\xcf\"W\xc5\xe6i\xc2!\xc1\u007f\x03\x00\x00\xff\xff'B.\x809\f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYQs\x1b\xb9\r~\u05ef\xc0\xf8\x1eܛ\x89\xa4I\xda\xe9t\xf4v\xb1{\x1d\xb7w\x8e'r\xf3\x92\xc9\x03\xb4\x84\xb4\xacw\t\x96\xc4JQ;\xfd\xef\x1d\x90\xbb\x92VZ\xcb\xf2]&\xfb\x92\x98\x04A\xe0#\x00~\xa0F\xe3\xf1x\x84\xde~\xa2\x10-\xbb\x19\xa0\xb7\xf4U\xc8\xe9_q\xf2\xf4\x978\xb1<]\xbf]\x90\xe0\xdbѓuf\x067M\x14\xae?R\xe4&\x14tKK\xeb\xacXv\xa3\x9a\x04\r\n\xceF\x00E \xd4\xc1G[S\x14\xac\xfd\f\\SU#\x00\x875\xcd\xc0\xb3Ys\xd5Դ\xc0\xe2\xa9\xf1q\xb2\xa6\x8a\x02O,\x8f\xa2\xa7BU\xac\x027~\x06\xfb\x89\xbc6\xea\x1c@\xb6\xe5\x81ͧ\xa4\xe6}R\x93f*\x1b\xe5\x1fC\xb3\xbf\xd8(I\xc2WM\xc0\xeaԈ4\x19\xad[5\x15\x86\x93\xe9\x11@,\xd8\xd3\f\xae\xaeF\x00k\xac\xacI>f\x83ؓ\xfb\xe9\xe1\xee\xd3\x1f\xe7EI5\xe6A\x00\x1f\xd8S\x10\xdb٭\xdf\x01\xe0\xbb1\x00C\xb1\b\xd6'\x8dp\xad\xaa\xb2\f\x18\x85\x98\"HI\xb0\xcecd \xa6m\x80\x97 \xa5\x8d\x10\xc8\a\x8a\xe4$\x99t\xa0\x16T\x04\x1d\xf0\xe2_T\xc8\x04\xe6\x14T\tĒ\x9b\xca@\xc1nMA P\xc1+g\xff\xb3\xd3\x1cA8mY\xa1P\v\\\xf7Y'\x14\x1cV\nBCo\x00\x9d\x81\x1a\xb7\x10H\xf7\x80\xc6\x1dhK\"q\x02\xbfr \xb0n\xc93(E|\x9cM\xa7++]\x88\x15\\\u05cd\xb3\xb2\x9d\x16\xec$\xd8E#\x1c\xe2\xd4К\xaa)z;Nv:IaY\x9b\x1fB\x1b~\xf1\xfa\xc00\xd9\xea\xe9D\t֭v\xc3)P\x9e\x85Y\x03\x05l\x04l\x97es\xf7hꐂ\xf0\xf1\xaf\xf3G\xe86M\x88\xf7!N\xe0\xee\x97\xc5=Ί\x8buK\n\xf9\x9c\x96\x81뤑\x9c\xf1l\x9d\xa4?\x8aʒ\xebc\x1c\x9bEmE\x0f\xf6\xdf\rE\xd1\xe3\x98\xc0\r:\xc7\x02\v\x82\xc6\x1b\x142\x13\xb8sp\x835U7\x18\xe9[\xa3\xac\x80Ʊ\"\xf82·\xd9\xdf\x17\xcc\xe0솻\xfc\x1e<\x90\xa3\x94\x9d{*\xf4x\x14#]g\x97\xb6H\x01\x0eK\x0e\x80\xc7\xe2\x93\x03\xb5C\x89\xa7_N\xe6\xb9p\xc0\x15\xfd\xc2\xc5A\n?c\xd3\xfb\xa1\x15\x9dUZ\x92r\x12R\xab\x1ab\x96\x81o\x91\x01&\x03\xfd\x83>w\xd8\xf0l\xb5\x1d\xb4\xf4\xa7\x87\xbb\xae\xc2v \xb56\xcb\xf1\x8eg\x11\xd1oi\xa92\x0f(勻^\xdf-\xf36\xa9\x00\t\x03\x82\xb7TP\xafp\x83uQ\bM\x1e\x1cP\t\xa0\x89\x1b\xa8\x95\u007f\x93\xcbM[\xd5\xf6\xc5^\xb1\x06̷\x17\xfc}\xfe\xe1~\xfa7ζ\x0e\xeaĢ\xa0\xa8jP\xa8&'o 6E\t\x18\xd5\x05\x1b\xc8\xccufR\xa3\xb3K\x8a2iw\xa0\x10?\xbf\xfb2\x84\x19\xc0\xcf\x1c\x80\xbeb\xed+z\x036\xa3\xbc\xab\x9f]\x80ؘ\x81\xd8郍\x95\xd2\x0e;\x8e\x1aH\xadÛ\xe4\xa8\xe0\x13\x01\xb7\x8e6\x04\x95}\xd2{[Kȁ\x89\xff\xd5l\xf8\xdf\u0560\xce?\xe4$\xbdR\x91\xabl\xd8\xeeF~\xb8\xfd0\xcbVi\b\xadR%\xd5Kmi\x95Q(\x95\xc8\x17\xa5\xc6d\x82\xa3\xc9\xc1!\fE\x89n\xa0\xb0B\xa2$\t\xdde#M\xa0\xc9\xf5k\xb3\xf5\x98%t\xdf\x00[8.\f\xdf\xe7νȋĭ_\xf4\xe2\xfe |\xcfz\xf1\xd4,(8\x12J\x8e\x18.\xa2\xfaP\x90\x978\xe55\x85\xb5\xa5\xcdt\xc3\xe1ɺ\xd5X\xe3n\x9c\x0f>N\x13Q\x9f\xfe\x90\xfe\xf9M^D\x8fŅ\xae$\xd1\xef\xe1\x8f\xee\x13\xa7\xafv\xa7c\x8d\x97^B\xd7\xf3\x96\xe8\x1c\xaf\xd4\fؔ\xb6(;ƿ/\x96\x83)Q\xa3\xc9\x15\x16\xdd\xf6[G\xa9\xe2\xd6\x04\xdd~;N+\xb8\x1a\xa33\xfa\xffh\xa3\xe8\xf8\xab\x81j\xec\x05)\xf8ϻ\xdb\xef\x13\xbb\x8d}u\x02\x0e\xd2\xdd\x1c\x02\x9e\xef\x8c·\xb4\x14Β\xa5\x8f=ю\xb6\r\xf0ĝ\xcc\xc5\xed\xe8S\x8f\xb0\a\xed`u\xf7@\x90\xf5\xb4-O\x81.=\x99i\xce\b\x83\xb1\xd1Wx\xda\xf3t.$&\xa1)\xa3)\xbd\x8f\xd6.M=\x854\xf5\x9a7\x88d\xcd-\xbbAJ\xdb\xe5\xa7u\xf2\xe7?=\xcb8\xac\x13Z\xf5\x8az;\xab\x00\xbeW\xfd\xdfZ\xf7\xb3\x17kt\xe8c\xc9rw{\xf6\xb4\xe7;\xb1.\xca\xf7\xa4%ծ\xf4\xaa\xd7\nuG\u07bf\xd2\xf2\x97\xc3\xe0\xe2ԋ\x82A.\xbb<\xe6=\xd1\x17\ue364\x97\xcc\x04\xe6\xe41\xa0\x9c\x06fzܽ9\xfe\xe9\xe3\rD\x9b\x1e\xbd\x94\xfbd2\x94\x1b٨\u05c9R;\x0e9VO5\xf6.\x82^\xe1\xef\x9b\xfe=j\xfe@<\x1c\r\xad\xbb\x9f\x91\xd6o\xf7\u007f\xa5\xb8\x1c\xb7\xbf\xfb\xa4\x89\xd6-s\xb0y\xfbfڎ\xeci\b\x16J\xd9\xc9\xdc\x1f\xff\xf2s\x95\x1fB\xba\x9frҟ\x05\xbb\xccf\xe3\f>\u007f\x19A\xfb\x92\xfa\xa9\xb3C\a\xff\x1f\x00\x00\xff\xff\r_=\xe6\xf2\x1a\x00\x00"), diff --git a/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml b/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml index b632bab72..321774e99 100644 --- a/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml +++ b/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml @@ -80,18 +80,20 @@ spec: - ReadWrite type: string lastSyncedRevision: - description: UID is a type that holds unique ID values, including UUIDs. Because - we don't ONLY use UUIDs, this is an alias to string. Being a type - captures intent and helps make sure that UIDs and names do not get - conflated. + description: "LastSyncedRevision is the value of the `metadata/revision` + file in the backup storage location the last time the BSL's contents + were synced into the cluster. \n Deprecated: this field is no longer + updated or used for detecting changes to the location's contents and + will be removed entirely in v2.0." type: string lastSyncedTime: + description: LastSyncedTime is the last time the contents of the location + were synced into the cluster. format: date-time nullable: true type: string phase: - description: BackupStorageLocationPhase is the lifecyle phase of a Velero - BackupStorageLocation. + description: Phase is the current state of the BackupStorageLocation. enum: - Available - Unavailable diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index b0eb0d6d4..184f02195 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -175,27 +175,6 @@ func (_m *BackupStore) GetPodVolumeBackups(name string) ([]*v1.PodVolumeBackup, return r0, r1 } -// GetRevision provides a mock function with given fields: -func (_m *BackupStore) GetRevision() (string, error) { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // IsValid provides a mock function with given fields: func (_m *BackupStore) IsValid() error { ret := _m.Called() diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 69ef80e2a..f9b751bbc 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -25,7 +25,6 @@ import ( "time" "github.com/pkg/errors" - uuid "github.com/satori/go.uuid" "github.com/sirupsen/logrus" kerrors "k8s.io/apimachinery/pkg/util/errors" @@ -49,7 +48,6 @@ type BackupInfo struct { // Velero backup and restore data in/from a persistent backup store. type BackupStore interface { IsValid() error - GetRevision() (string, error) ListBackups() ([]string, error) @@ -249,10 +247,6 @@ func (s *objectBackupStore) PutBackup(info BackupInfo) error { return kerrors.NewAggregate(errs) } - if err := s.putRevision(); err != nil { - s.logger.WithField("backup", info.Name).WithError(err).Warn("Error updating backup store revision") - } - return nil } @@ -380,10 +374,6 @@ func (s *objectBackupStore) DeleteBackup(name string) error { } } - if err := s.putRevision(); err != nil { - s.logger.WithField("backup", name).WithError(err).Warn("Error updating backup store revision") - } - return errors.WithStack(kerrors.NewAggregate(errs)) } @@ -403,10 +393,6 @@ func (s *objectBackupStore) DeleteRestore(name string) error { } } - if err = s.putRevision(); err != nil { - errs = append(errs, err) - } - return errors.WithStack(kerrors.NewAggregate(errs)) } @@ -437,30 +423,6 @@ func (s *objectBackupStore) GetDownloadURL(target velerov1api.DownloadTarget) (s } } -func (s *objectBackupStore) GetRevision() (string, error) { - rdr, err := s.objectStore.GetObject(s.bucket, s.layout.getRevisionKey()) - if err != nil { - return "", err - } - - bytes, err := ioutil.ReadAll(rdr) - if err != nil { - return "", errors.Wrap(err, "error reading contents of revision file") - } - - return string(bytes), nil -} - -func (s *objectBackupStore) putRevision() error { - rdr := strings.NewReader(uuid.NewV4().String()) - - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getRevisionKey(), rdr); err != nil { - return errors.Wrap(err, "error updating revision file") - } - - return nil -} - func seekToBeginning(r io.Reader) error { seeker, ok := r.(io.Seeker) if !ok { diff --git a/pkg/persistence/object_store_layout.go b/pkg/persistence/object_store_layout.go index 40fe610c7..afea93a70 100644 --- a/pkg/persistence/object_store_layout.go +++ b/pkg/persistence/object_store_layout.go @@ -59,10 +59,6 @@ func (l *ObjectStoreLayout) isValidSubdir(name string) bool { return ok } -func (l *ObjectStoreLayout) getRevisionKey() string { - return path.Join(l.subdirs["metadata"], "revision") -} - func (l *ObjectStoreLayout) getBackupDir(backup string) string { return path.Join(l.subdirs["backups"], backup) + "/" } diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index 578ccc0c8..4ccc05834 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -23,13 +23,11 @@ import ( "errors" "io" "io/ioutil" - "path" "sort" "strings" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -237,7 +235,6 @@ func TestPutBackup(t *testing.T) { "backups/backup-1/backup-1-podvolumebackups.json.gz", "backups/backup-1/backup-1-volumesnapshots.json.gz", "backups/backup-1/backup-1-resource-list.json.gz", - "metadata/revision", }, }, { @@ -257,7 +254,6 @@ func TestPutBackup(t *testing.T) { "prefix-1/backups/backup-1/backup-1-podvolumebackups.json.gz", "prefix-1/backups/backup-1/backup-1-volumesnapshots.json.gz", "prefix-1/backups/backup-1/backup-1-resource-list.json.gz", - "prefix-1/metadata/revision", }, }, { @@ -296,7 +292,6 @@ func TestPutBackup(t *testing.T) { "backups/backup-1/backup-1-podvolumebackups.json.gz", "backups/backup-1/backup-1-volumesnapshots.json.gz", "backups/backup-1/backup-1-resource-list.json.gz", - "metadata/revision", }, }, { @@ -480,7 +475,6 @@ func TestDeleteBackup(t *testing.T) { } objectStore.On("DeleteObject", backupStore.bucket, obj).Return(err) - objectStore.On("PutObject", "test-bucket", path.Join(test.prefix, "metadata", "revision"), mock.Anything).Return(nil) } err := backupStore.DeleteBackup("bak")