diff --git a/pkg/apis/velero/v1/backup.go b/pkg/apis/velero/v1/backup.go index 5d20eedf5..6c978cef9 100644 --- a/pkg/apis/velero/v1/backup.go +++ b/pkg/apis/velero/v1/backup.go @@ -212,10 +212,15 @@ const ( // BackupStatus captures the current status of a Velero backup. type BackupStatus struct { - // Version is the backup format version. + // Version is the backup format major version. + // Deprecated: Please see FormatVersion // +optional Version int `json:"version,omitempty"` + // FormatVersion is the backup format version, including major, minor, and patch version. + // +optional + FormatVersion string `json:"formatVersion,omitempty"` + // Expiration is when this Backup is eligible for garbage-collection. // +optional // +nullable diff --git a/pkg/apis/velero/v1/constants.go b/pkg/apis/velero/v1/constants.go index 2d216858f..a7292d568 100644 --- a/pkg/apis/velero/v1/constants.go +++ b/pkg/apis/velero/v1/constants.go @@ -39,4 +39,11 @@ const ( // CSIFeatureFlag is the feature flag string that defines whether or not CSI features are being used. CSIFeatureFlag = "EnableCSI" + + // PreferredVersionDir is the suffix name of the directory containing the preferred version of the API group + // resource within a Velero backup. + PreferredVersionDir = "-preferredversion" + + // APIGroupVersionsFeatureFlag is the feature flag string that defines whether or not to handle multiple API Group Versions + APIGroupVersionsFeatureFlag = "EnableAPIGroupVersions" ) diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 4878f5151..850bee001 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -40,9 +40,13 @@ import ( "github.com/vmware-tanzu/velero/pkg/util/collections" ) -// BackupVersion is the current backup version for Velero. +// BackupVersion is the current backup major version for Velero. +// Deprecated, use BackupFormatVersion const BackupVersion = 1 +// BackupFormatVersion is the current backup version for Velero, including major, minor, and patch. +const BackupFormatVersion = "1.1.0" + // Backupper performs backups. type Backupper interface { // Backup takes a backup using the specification in the api.Backup and writes backup and log data @@ -284,7 +288,7 @@ func (kb *kubernetesBackupper) Backup(log logrus.FieldLogger, backupRequest *Req func (kb *kubernetesBackupper) writeBackupVersion(tw *tar.Writer) error { versionFile := filepath.Join(api.MetadataDir, "version") - versionString := fmt.Sprintf("%d\n", BackupVersion) + versionString := fmt.Sprintf("%s\n", BackupFormatVersion) hdr := &tar.Header{ Name: versionFile, diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 73039daa7..aafbb8845 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -99,6 +99,16 @@ func TestBackedUpItemsMatchesTarballContents(t *testing.T) { } file = file + "/" + item.name + ".json" expectedFiles = append(expectedFiles, file) + + fileWithVersion := "resources/" + gvkToResource[item.resource] + if item.namespace != "" { + fileWithVersion = fileWithVersion + "/v1-preferredversion/" + "namespaces/" + item.namespace + } else { + file = file + "/cluster" + fileWithVersion = fileWithVersion + "/v1-preferredversion" + "/cluster" + } + fileWithVersion = fileWithVersion + "/" + item.name + ".json" + expectedFiles = append(expectedFiles, fileWithVersion) } assertTarballContents(t, backupFile, append(expectedFiles, "metadata/version")...) @@ -135,6 +145,10 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/zoo/raz.json", "resources/deployments.apps/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -155,6 +169,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -175,6 +191,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -195,6 +213,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", }, }, { @@ -215,6 +235,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", }, }, { @@ -241,6 +263,10 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/zoo/raz.json", "resources/deployments.apps/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -266,6 +292,9 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", "resources/persistentvolumes/cluster/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/persistentvolumes/v1-preferredversion/cluster/bar.json", }, }, { @@ -290,6 +319,9 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/zoo/raz.json", "resources/deployments.apps/namespaces/foo/bar.json", "resources/persistentvolumes/cluster/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/persistentvolumes/v1-preferredversion/cluster/bar.json", }, }, { @@ -314,6 +346,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/zoo/raz.json", "resources/persistentvolumes/cluster/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/persistentvolumes/v1-preferredversion/cluster/bar.json", }, }, { @@ -341,6 +375,12 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/deployments.apps/namespaces/zoo/raz.json", "resources/persistentvolumes/cluster/bar.json", "resources/persistentvolumes/cluster/baz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/persistentvolumes/v1-preferredversion/cluster/bar.json", + "resources/persistentvolumes/v1-preferredversion/cluster/baz.json", }, }, { @@ -365,6 +405,10 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/ns-2/pod-1.json", "resources/persistentvolumes/cluster/pv-1.json", "resources/persistentvolumes/cluster/pv-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-2.json", }, }, { @@ -387,6 +431,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-1.json", }, }, { @@ -408,6 +454,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-1.json", }, }, { @@ -432,6 +480,11 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/ns-3/pod-1.json", "resources/persistentvolumes/cluster/pv-1.json", "resources/persistentvolumes/cluster/pv-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-3/pod-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-2.json", }, }, { @@ -454,6 +507,9 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-1.json", "resources/pods/namespaces/ns-3/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-3/pod-1.json", }, }, { @@ -477,6 +533,11 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/ns-3/pod-1.json", "resources/persistentvolumes/cluster/pv-1.json", "resources/persistentvolumes/cluster/pv-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-3/pod-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-2.json", }, }, { @@ -499,6 +560,10 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/zoo/raz.json", "resources/deployments.apps/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -521,6 +586,10 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/namespaces/zoo/raz.json", "resources/deployments.apps/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -541,6 +610,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -578,6 +649,8 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -599,8 +672,12 @@ func TestBackupResourceFiltering(t *testing.T) { want: []string{ "resources/pods/namespaces/foo/bar.json", "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", "resources/deployments.apps/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -614,6 +691,7 @@ func TestBackupResourceFiltering(t *testing.T) { }, want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", }, }, } @@ -668,6 +746,10 @@ func TestCRDInclusion(t *testing.T) { "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/volumesnapshotlocations.velero.io.json", "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/test.velero.io.json", "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/backups.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/volumesnapshotlocations.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/test.velero.io.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", }, }, { @@ -687,6 +769,7 @@ func TestCRDInclusion(t *testing.T) { }, want: []string{ "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", }, }, { @@ -709,6 +792,10 @@ func TestCRDInclusion(t *testing.T) { "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/volumesnapshotlocations.velero.io.json", "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/test.velero.io.json", "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/backups.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/volumesnapshotlocations.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/test.velero.io.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", }, }, { @@ -729,6 +816,8 @@ func TestCRDInclusion(t *testing.T) { want: []string{ "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/volumesnapshotlocations.velero.io.json", "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/volumesnapshotlocations.velero.io.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", }, }, { @@ -749,6 +838,7 @@ func TestCRDInclusion(t *testing.T) { }, want: []string{ "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", }, }, { @@ -772,6 +862,10 @@ func TestCRDInclusion(t *testing.T) { "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/volumesnapshotlocations.velero.io.json", "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/test.velero.io.json", "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/backups.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/volumesnapshotlocations.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/test.velero.io.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", }, }, } @@ -818,6 +912,8 @@ func TestBackupResourceCohabitation(t *testing.T) { want: []string{ "resources/deployments.extensions/namespaces/foo/bar.json", "resources/deployments.extensions/namespaces/zoo/raz.json", + "resources/deployments.extensions/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.extensions/v1-preferredversion/namespaces/zoo/raz.json", }, }, { @@ -836,6 +932,8 @@ func TestBackupResourceCohabitation(t *testing.T) { want: []string{ "resources/deployments.apps/namespaces/foo/bar.json", "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", }, }, } @@ -877,7 +975,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) { h.backupper.Backup(h.log, backup1, backup1File, nil, nil) - assertTarballContents(t, backup1File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json") + assertTarballContents(t, backup1File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json", "resources/deployments.apps/v1-preferredversion/namespaces/ns-1/deploy-1.json") // run and verify backup 2 backup2 := &Request{ @@ -887,7 +985,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) { h.backupper.Backup(h.log, backup2, backup2File, nil, nil) - assertTarballContents(t, backup2File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json") + assertTarballContents(t, backup2File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json", "resources/deployments.apps/v1-preferredversion/namespaces/ns-1/deploy-1.json") } // TestBackupResourceOrdering runs backups of the core API group and ensures that items are backed @@ -1431,6 +1529,9 @@ func TestBackupActionAdditionalItems(t *testing.T) { "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-2.json", "resources/pods/namespaces/ns-3/pod-3.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-3/pod-3.json", }, }, { @@ -1457,6 +1558,7 @@ func TestBackupActionAdditionalItems(t *testing.T) { }, want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", }, }, { @@ -1488,6 +1590,9 @@ func TestBackupActionAdditionalItems(t *testing.T) { "resources/pods/namespaces/ns-1/pod-1.json", "resources/persistentvolumes/cluster/pv-1.json", "resources/persistentvolumes/cluster/pv-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-2.json", }, }, { @@ -1516,6 +1621,7 @@ func TestBackupActionAdditionalItems(t *testing.T) { }, want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", }, }, { @@ -1546,6 +1652,8 @@ func TestBackupActionAdditionalItems(t *testing.T) { want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", }, }, { @@ -1575,6 +1683,8 @@ func TestBackupActionAdditionalItems(t *testing.T) { want: []string{ "resources/pods/namespaces/ns-1/pod-1.json", "resources/persistentvolumes/cluster/pv-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/pv-2.json", }, }, @@ -1604,6 +1714,8 @@ func TestBackupActionAdditionalItems(t *testing.T) { want: []string{ "resources/pods/namespaces/ns-2/pod-2.json", "resources/pods/namespaces/ns-3/pod-3.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-3/pod-3.json", }, }, } @@ -2194,6 +2306,8 @@ func TestBackupWithHooks(t *testing.T) { wantBackedUp: []string{ "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", }, }, { @@ -2243,6 +2357,8 @@ func TestBackupWithHooks(t *testing.T) { wantBackedUp: []string{ "resources/pods/namespaces/ns-1/pod-1.json", "resources/pods/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", }, }, { @@ -2297,6 +2413,7 @@ func TestBackupWithHooks(t *testing.T) { }, wantBackedUp: []string{ "resources/pods/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", }, }, { @@ -2348,6 +2465,7 @@ func TestBackupWithHooks(t *testing.T) { }, wantBackedUp: []string{ "resources/pods/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", }, }, } diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index 1640914e8..8ea7b6b52 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -91,7 +91,7 @@ func (f *defaultItemBackupperFactory) newItemBackupper( } type ItemBackupper interface { - backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource) (bool, error) + backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource, preferredGVR schema.GroupVersionResource) (bool, error) } type defaultItemBackupper struct { @@ -112,7 +112,7 @@ type defaultItemBackupper struct { // namespaces IncludesExcludes list. // In addition to the error return, backupItem also returns a bool indicating whether the item // was actually backed up. -func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource) (bool, error) { +func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource, preferredGVR schema.GroupVersionResource) (bool, error) { metadata, err := meta.Accessor(obj) if err != nil { return false, err @@ -254,11 +254,27 @@ func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtim return false, kubeerrs.NewAggregate(backupErrs) } + // group version of this object + // Used on filepath to backup up all groups and versions + version := resourceVersion(obj) + + // Getting the preferred group version of this resource + preferredVersion := preferredGVR.Version + var filePath string + + // API Group version is now part of path of backup as a subdirectory + // it will add a prefix to subdirectory name for the preferred version + versionPath := version + + if version == preferredVersion { + versionPath = version + api.PreferredVersionDir + } + if namespace != "" { - filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.NamespaceScopedDir, namespace, name+".json") + filePath = filepath.Join(api.ResourcesDir, groupResource.String(), versionPath, api.NamespaceScopedDir, namespace, name+".json") } else { - filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.ClusterScopedDir, name+".json") + filePath = filepath.Join(api.ResourcesDir, groupResource.String(), versionPath, api.ClusterScopedDir, name+".json") } itemBytes, err := json.Marshal(obj.UnstructuredContent()) @@ -282,6 +298,33 @@ func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtim return false, errors.WithStack(err) } + // backing up the preferred version backup without API Group version on path - this is for backward compability + + if version == preferredVersion { + if namespace != "" { + filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.NamespaceScopedDir, namespace, name+".json") + } else { + filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.ClusterScopedDir, name+".json") + } + + hdr = &tar.Header{ + Name: filePath, + Size: int64(len(itemBytes)), + Typeflag: tar.TypeReg, + Mode: 0755, + ModTime: time.Now(), + } + + if err := ib.tarWriter.WriteHeader(hdr); err != nil { + return false, errors.WithStack(err) + } + + if _, err := ib.tarWriter.Write(itemBytes); err != nil { + return false, errors.WithStack(err) + } + + } + return true, nil } @@ -352,7 +395,7 @@ func (ib *defaultItemBackupper) executeActions( return nil, errors.WithStack(err) } - if _, err = ib.additionalItemBackupper.backupItem(log, additionalItem, gvr.GroupResource()); err != nil { + if _, err = ib.additionalItemBackupper.backupItem(log, additionalItem, gvr.GroupResource(), gvr); err != nil { return nil, err } } @@ -524,3 +567,10 @@ func resourceKey(obj runtime.Unstructured) string { gvk := obj.GetObjectKind().GroupVersionKind() return fmt.Sprintf("%s/%s", gvk.GroupVersion().String(), gvk.Kind) } + +// resourceVersion returns a string representing the object's API Version (e.g. +// v1 if item belongs to apps/v1 +func resourceVersion(obj runtime.Unstructured) string { + gvk := obj.GetObjectKind().GroupVersionKind() + return gvk.Version +} diff --git a/pkg/backup/resource_backupper.go b/pkg/backup/resource_backupper.go index 9e8edb8b1..af6b21da7 100644 --- a/pkg/backup/resource_backupper.go +++ b/pkg/backup/resource_backupper.go @@ -110,6 +110,12 @@ func (rb *defaultResourceBackupper) backupResource(group *metav1.APIResourceList } gr := schema.GroupResource{Group: gv.Group, Resource: resource.Name} + // Getting the preferred group version of this resource + preferredGVR, _, err := rb.discoveryHelper.ResourceFor(gr.WithVersion("")) + if err != nil { + return errors.WithStack(err) + } + clusterScoped := !resource.Namespaced // If the resource we are backing up is NOT namespaces, and it is cluster-scoped, check to see if @@ -194,7 +200,7 @@ func (rb *defaultResourceBackupper) backupResource(group *metav1.APIResourceList continue } - if _, err := itemBackupper.backupItem(log, unstructured, gr); err != nil { + if _, err := itemBackupper.backupItem(log, unstructured, gr, preferredGVR); err != nil { log.WithError(errors.WithStack(err)).Error("Error backing up namespace") } } @@ -233,7 +239,7 @@ func (rb *defaultResourceBackupper) backupResource(group *metav1.APIResourceList // do the backup for _, item := range unstructuredList.Items { - if rb.backupItem(log, gr, itemBackupper, &item) { + if rb.backupItem(log, gr, itemBackupper, &item, preferredGVR) { backedUpItem = true } } @@ -254,6 +260,7 @@ func (rb *defaultResourceBackupper) backupItem( gr schema.GroupResource, itemBackupper ItemBackupper, unstructured runtime.Unstructured, + preferredGVR schema.GroupVersionResource, ) bool { metadata, err := meta.Accessor(unstructured) if err != nil { @@ -271,7 +278,7 @@ func (rb *defaultResourceBackupper) backupItem( return false } - backedUpItem, err := itemBackupper.backupItem(log, unstructured, gr) + backedUpItem, err := itemBackupper.backupItem(log, unstructured, gr, preferredGVR) if aggregate, ok := err.(kubeerrs.Aggregate); ok { log.Infof("%d errors encountered backup up item", len(aggregate.Errors())) // log each error separately so we get error location info in the log, and an @@ -324,7 +331,7 @@ func (rb *defaultResourceBackupper) backupCRD(log logrus.FieldLogger, gr schema. } log.Infof("Found associated CRD %s to add to backup", gr.String()) - rb.backupItem(log, gvr.GroupResource(), itemBackupper, unstructured) + rb.backupItem(log, gvr.GroupResource(), itemBackupper, unstructured, gvr) } // getNamespacesToList examines ie and resolves the includes and excludes to a full list of diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 8b255abb4..a2224c25a 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -320,9 +320,12 @@ func (c *backupController) prepareBackupRequest(backup *velerov1api.Backup) *pkg Backup: backup.DeepCopy(), // don't modify items in the cache } - // set backup version + // set backup major version - deprecated, use Status.FormatVersion request.Status.Version = pkgbackup.BackupVersion + // set backup major, minor, and patch version + request.Status.FormatVersion = pkgbackup.BackupFormatVersion + if request.Spec.TTL.Duration == 0 { // set default backup TTL request.Spec.TTL.Duration = c.defaultBackupTTL diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 24d0af168..87d457a93 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -374,6 +374,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseCompleted, Version: 1, + FormatVersion: "1.1.0", StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, Expiration: ×tamp, @@ -407,6 +408,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseCompleted, Version: 1, + FormatVersion: "1.1.0", StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, Expiration: ×tamp, @@ -443,6 +445,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseCompleted, Version: 1, + FormatVersion: "1.1.0", StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, Expiration: ×tamp, @@ -477,6 +480,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseCompleted, Version: 1, + FormatVersion: "1.1.0", Expiration: &metav1.Time{now.Add(10 * time.Minute)}, StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, @@ -511,6 +515,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseCompleted, Version: 1, + FormatVersion: "1.1.0", StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, Expiration: ×tamp, @@ -547,6 +552,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFailed, Version: 1, + FormatVersion: "1.1.0", StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, Expiration: ×tamp, @@ -581,6 +587,7 @@ func TestProcessBackupCompletions(t *testing.T) { Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFailed, Version: 1, + FormatVersion: "1.1.0", StartTimestamp: ×tamp, CompletionTimestamp: ×tamp, Expiration: ×tamp, diff --git a/pkg/discovery/helper.go b/pkg/discovery/helper.go index 560ff7076..e3f6fadcc 100644 --- a/pkg/discovery/helper.go +++ b/pkg/discovery/helper.go @@ -29,6 +29,8 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/restmapper" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/features" kcmdutil "github.com/vmware-tanzu/velero/third_party/kubernetes/pkg/kubectl/cmd/util" ) @@ -57,7 +59,11 @@ type Helper interface { } type serverResourcesInterface interface { + // ServerPreferredResources() is used to populate Resources() with only Preferred Versions - this is the default ServerPreferredResources() ([]*metav1.APIResourceList, error) + // ServerGroupsAndResources returns supported groups and resources for *all* groups and versions + // Used to populate Resources() if feature flag is passed + ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) } type helper struct { @@ -112,14 +118,28 @@ func (h *helper) Refresh() error { return errors.WithStack(err) } - preferredResources, err := refreshServerPreferredResources(h.discoveryClient, h.logger) - if err != nil { - return errors.WithStack(err) + var serverResources []*metav1.APIResourceList + + if features.IsEnabled(velerov1api.APIGroupVersionsFeatureFlag) { + // ServerGroupsAndResources returns all APIGroup and APIResouceList - not only preferred versions + _, serverAllResources, err := refreshServerGroupsAndResources(h.discoveryClient, h.logger) + if err != nil { + return errors.WithStack(err) + } + h.logger.Info("The '%s' feature flag was specified, using all API group versions.", velerov1api.APIGroupVersionsFeatureFlag) + serverResources = serverAllResources + } else { + // ServerPreferredResources() returns only preferred APIGroup - this is the default since no feature flag has been passed + serverPreferredResources, err := refreshServerPreferredResources(h.discoveryClient, h.logger) + if err != nil { + return errors.WithStack(err) + } + serverResources = serverPreferredResources } h.resources = discovery.FilteredBy( discovery.ResourcePredicateFunc(filterByVerbs), - preferredResources, + serverResources, ) sortResources(h.resources) @@ -172,6 +192,19 @@ func refreshServerPreferredResources(discoveryClient serverResourcesInterface, l return preferredResources, err } +func refreshServerGroupsAndResources(discoveryClient serverResourcesInterface, logger logrus.FieldLogger) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + serverGroups, serverResources, err := discoveryClient.ServerGroupsAndResources() + if err != nil { + if discoveryErr, ok := err.(*discovery.ErrGroupDiscoveryFailed); ok { + for groupVersion, err := range discoveryErr.Groups { + logger.WithError(err).Warnf("Failed to discover group: %v", groupVersion) + } + return serverGroups, serverResources, nil + } + } + return serverGroups, serverResources, err +} + func filterByVerbs(groupVersion string, r *metav1.APIResource) bool { return discovery.SupportsAllVerbs{Verbs: []string{"list", "create", "get", "delete"}}.Match(groupVersion, r) } diff --git a/pkg/discovery/helper_test.go b/pkg/discovery/helper_test.go index 997c50697..d63e2df11 100644 --- a/pkg/discovery/helper_test.go +++ b/pkg/discovery/helper_test.go @@ -151,6 +151,7 @@ func TestRefreshServerPreferredResources(t *testing.T) { tests := []struct { name string resourceList []*metav1.APIResourceList + apiGroup []*metav1.APIGroup failedGroups map[schema.GroupVersion]error returnError error }{ @@ -183,7 +184,7 @@ func TestRefreshServerPreferredResources(t *testing.T) { formatFlag := logging.FormatText for _, test := range tests { - fakeServer := velerotest.NewFakeServerResourcesInterface(test.resourceList, test.failedGroups, test.returnError) + fakeServer := velerotest.NewFakeServerResourcesInterface(test.resourceList, test.apiGroup, test.failedGroups, test.returnError) t.Run(test.name, func(t *testing.T) { resources, err := refreshServerPreferredResources(fakeServer, logging.DefaultLogger(logrus.DebugLevel, formatFlag)) if test.returnError != nil { diff --git a/pkg/generated/crds/crds.go b/pkg/generated/crds/crds.go index f7a96b1f7..940b92ef8 100644 --- a/pkg/generated/crds/crds.go +++ b/pkg/generated/crds/crds.go @@ -29,7 +29,7 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xecej/\xf9\xbfj\xc8\x06\xac\xa2%\x05\xb3\x18d\x1f\x1b\x97\x16\xb5d\u0089\xac\xc2+bD\xc1N\xa0ѭ\x01\x95lA\xa3!f\r\u007fV\x1a\x81˝\xba\x86\x83\xb5\xa5\xb9~\xfbv\xcfm\xb4\xa5L\x15E%\xb9=\xbd%\x8b\xe0\xdb\xca*m\xde\xe6xD\xf1\xd6\xf0\xfd\x8a\xe9\xec\xc0-fNxoY\xc9W\x84\xb8$SZ\x17\xf9\xffD\xa9\x9b7-L\xed\xc9)\x99\xb1\x9a\xcb}\xddM\xaa>\xc9w\xa7\xf3^\x9d\xfc4\x8f\u007f\xc3^\xd7\xe5\xb8\xf2\xd3\xed\xfdC[ո\xe9\xf2\x9c\xb8\xddL3\r\xe3\x1d\xa3\xb8ܡ\xf6\x82\xdbiU\x10D\x94\xb9\xd75RS\xc1Qv\x99n\xaam\xc1\xad\x93\xf4?+4N\x9d\xd5\x1anȣ\xc0\x16\xa1*s\xa7\x85k\xd8H\xb8a\x05\x8a\x1bf\xf0\x17g\xbb\xe3\xb0Y9\x96.3\xbe\xed\b\xbb\x03=\xb7\xea\xee\xe8\xb2F%\xe4-\xfe\xbeĬc\x18n\x0e\xdf\xf1\x8c\xd4\x1fvJ7\x0e\xc1\xfb\xa4u\v\xe0\x98Q\xba\x86/\x99\xa8r\xcck\xb7\xd4\xfb\xbd\x87\xca\xed`8\xf9sƥ\xd3\x1f\xe7A\x9d\xed\xc9\xe6W\xf2HLc\x0f(\x80\x93!\x97\x1e\x1a\xf9\x9a\x03\x8e\xa0M\xa6g\xb1\x18`5\xc1\xf0\x00\xbb\x12\x82m\x05^\x83\xd5U\u007fi?\x8fi\xcdN\xa3\x9c\x88\xfbY\x1a#\xea\xd1\xc1\x82\x04\xcf\xc8\xd3\xd6vB\xbc\xf8\r\xb1\xe1\xa0\xd4\xd3<\xe9\u007fr#\x1a;\x87\x8c\xc2\x00\xd8\xe2\x81\x1d\xb9ҁ\xd8\xe0l\xb7\b\xf8\x82Yei\xbf\xeb6f!\xe7\xbb\x1dj\a\xa5<0\x83\xc6;\xf7)\x16L)\xb1kzJl\x03\xfc\x1b\x911\x8d\x9e\xde)\x94\xe1\xf9\x80\x92\x90\x19r\xd77\xb7\x13˜\x1fy^1\x01\\\x1a\xcbd\xe6\xe9`5N}:`Z\x9c\x03l\xbd\xf1G\x9c\x1d\xef;\x8e@I\x04\xa5\xa1p>o8Ԍ\u0087Ir\xb7\xcc`\x0eʫ\xa1\xae\x04\x9a\xb0PN\xfe\xa5\xb1\xeb\xab\t\xc0\xb5\x14\xfc\x0e)\xd8\x16\x05\x18\x14\x98Y\xa5\xc7\xd80/Tߖ}\xd4\x04\xefF\xbcUp\x9a\xc1\x85\xb6\x1d\x95\x9a\x84\t\xf0|\xe0\xd9\xc1o^N_\b\n\xe4\n\r\xd9/+Kq\x1a'\x0e\xe6%\xedی\t7m֘\xfb\xb0\x86fݴE?״\x05\x8f\xd7\xe5e-\xfa\xdf\x0f+\xa3\xe3>[17\x83\x89\xaf\xa9\x98\x8e\x89\xdc\x05\xa0\x9b\x1d`Q\xda\xd3\x15p\x1b{]\x8c\xcb\xe8\xf84ɞz\xedߜ \xce\xd5\xe9M\u007f\xde+\xea\xf4ϔB\xbd\xf4oF\b\xe4\xec\uf0efO\x14\xc0\xc7\xf6\x9c+\xe0\xbbZ\x00\xf9\x15츰\xa8{\x92\x98#W\xcdK\xe2\xe7\xb2`y\xa7r\xad`6;ܾ\xb8\xe8\xc84Y\x8f$n\xf4\xa7\xfa\x982F\xd5\xdd\xcdt\x16*Б\x89k,\xfcA\xec\x818\xd8\xf4P\xe4\xf3\xfe\xd3\a̧\x99\x02)\x1a6 \xe1}\x0f\xcd\xf6\xb2!DN# \x04)\xf5\xe9\xc2\x1f\xaa\xaf\x80\xc1\x13\x9e|t\xe1\x8e\xf8%j\xe6\x96q\x83\x17!j\xa4\x93=)\xd4\x13\x9e\bH8\xac/\xccM\x13\xbdoOxZ\x1e\xd4c\x9bÆ\x9b\x90|p\xfcs\x1d\xc4\x00:饲\f(\xd5\x12=\xcc\x12Q\x90\xea\"b\x8b\xdc>\x9b\xbcZLMv\xc0\v\xf2\x8d\xf1Bq\xda~\xe0e\x12\x81\xceu\x82A\xb2\x89\x98jyd\x82\xe7\xf52^\xbf7\xf2\n>)\xbb\x91S\xc1j\xb7ݾp\x132\\\x1f\x14\x9aO\xcaRϫ3ѣ|6\v\xfd42!\xe9ݰ\xa3\xbf\x9d\xb1YTb\xdf6\xfe\x84U\x8b\x84\x1b\xd8Hw\x86\xf0\xbc\xf297\xbf\u061c\xb7ﶢ2\x94\x92\x91J\xaeh\xb3[\x8f\xad\x13X\x9c\xa8\xc8m)\fѪ\x97\xf4\xcb%A|p\xfb\x82\x9f\xed\xf3\x87\x82e\x98C^\x11\x13)\xff\xc5,\xeey\x06\x05\xea\xfd\xf4F\xd0n\xa5\xf3\xd9)\xcb'\xf9R\xdf\xceҧ\x94\xad9\xb6\xe0\x8c\xf3%4V\xce6\x17\xc7D\xd1.\f\x1cMxM\x0f\\\xa2\x836I\x8a\x1b\x16\xb8\xc9\xf2\x9c\xae#\x98\xb8K\xf6\xdeɜ\x1f\xee\xdb\x1e%\xbf\xc7\x15\xact\xd6\xf9o\xb7U\x91\xd2\xfe\aJ\xc6\xf5\xa2\x85\xbe\xa7{\x05\x81\x9d\x99!+\xd4^\xc4\xc1\xe7\x06\x9c4\x8fL\xf4Ӧ#d)\xe75P\xf8mX\xed\x06\x91\xc6\x15<\x1f\x94\xf1\xbb⎣ȁ\xcfEZ\xae]>\xe1\xe9\xf2j`\xe3\x97\x1by\xe9\xb7\xe7\x81\xc5ƽ|\x01\xb0\x92\xe2\x04\x974\xf3\xf2\xf3C\x97$\xadK\x18D\x97Li\xc1\xac;\xcd\xc5]\xdcM\xabo*\\(:\x8dm\x82Ε\xca\xd8D$\ue531>C\xd7\t\x1eGrC\xf3g\x9a\x90\x13\x02\xb6\xf3\xb7CJ\xc7{\x00\xe7\xc8z\xa9J'%\x83\xa3\t\xce\x01\xc4<\x80dB\xc0ec\xa3\xde?^\xfa\xcb\x01Z\x82e\x14\x16\xcc@t\xaaPj\x95\xa11s\xea\xb0\xe8y\x17\x12nu\xb2\x8d\xf9C\x85O\xb5\xcf%\xf7bK\r\x1b\x1dk\xce\n\xb3o_Z9@g\xda\xee\xffy5;\x0f#\xa0\x9bڢ`rq\xb3\x18 w\xe3\xe7ES\b`|Ȯ\xf7\x15\x99qj\xa4\x17\x94\xe6\xcbn\xb0\x05\x97\x1b\x02\x0e\xef^u;\x86\xe8\x12\xf1\xfc\x90\xfa&\xcel\xd8\\wx\xdb,\xd50\xe5>֞\x0f\xa8\xb1#\xa9af\x98\xc29\xa9l\xebx\x9e\xc6h\x8f\xc7\x1b\x03;\xae\x8dm#i\xa0\x9a\xb5\xda\x16\x90\xb3\xce(\xf2V\xeb\xcf8\xa2\xfc\xe8\xe7\xb5\x12@\a\xf5\x1c\xef\xd3\xa0=\xa0\x8e\x85\xcf+\xaa\xcb\x1eʹ\xb9\xfao\xdc|]\xa8\xe7\x94?*\xaf/\xbf\x9c-\xddK(\xc4\xdb*%\x90\xc91\xfaSk3G\x8aJ\xba5\x89uaG,JTq\x89\x01\xf5\xb1\xdeۅ\x99\xed\n\x06&D\xbb6\x85\xe9\x86)_\xa8^q\xb1\xf4c\xa1\xe0c\xbels\x9aC\xbdо\xcb\"\xdd)1\xfc\xc2\x1c\x9a\xad˘\xae\xc6\b7\x19h\xd9\xf1ݺ\xfb\x8bU\xa16\x03\x9e\xb9=\f\b\xa0\xa2Iwd\x91\xfbvqdԩPd\xdf\xe7\x1c(\r\x92\x8b\xabѺ\x98\xfa\xfdA\x9b\x9d\xf0c\xe9\x0fEg\xd9\xdb\\h\x9fR\xbb\xf1\xd9\x15\x1bݚ\x8cQ'{\xdeeGj\tizMF\xb7\xe6bb\x93I\xa8\xc48\xbb\xd2b\xf9\xbc5[U\xf1\x19\xb5\x14\xb1NbnÝ\xa9\xa0H\x889\x96\xab%>\xabF\x82.\xf3f\xb0>\xab2\xa2U\xf50\x032\xad\x1e\"\x81%K\xb5\x0fgW<\xf4\xab\ff\x88X\xaas\x98\xaea\x98\x01:ZݐR\xb90\x03\xb3\xaeix\xc5z\x85\x85*\x85ש$\xfc\xb9\xb1\xe7T\xcd\xc1B\xa5\xc1Bd:\x87\xd5B-Az\x05\xc1\x02\u007f>\xb3Z\xa0\xae\a\x18]\xf3\xdc\x1a\x81n\x15\xc0(\xc8\xc4ʀ\x89\xbb\xffQ\x90\t\xf5\x00\v7\xfe\xa3`g7\xc6\x19\x8d\x98\xfc\xc9HV\x9a\x83\xb2\x8f\xf4\xa4q>\x8a\xbc\xef\x8e\x1d9\\\xb8\x18\x87=!dBUy\r{H\n=T<\xc1\xdd#9yz\n\x935\x0f\x81\x82+\x8f\xc1O\xff\x9d\xd0w\xafy\xd80Vi\xb6Ǐ*k\xbdG\x9d\xa2\xbf;\xb6\xf3x0\b5\x1e\xe9c\x1d\x04\x8b\xafغS\xc7bǐe\xf3\xf1a\xeb\xf4\xe50\x1c\xca{\xd2\xf2\xac\x15\xb3D<<|\xf4\x88[^\xe0\xfaC\xe5\x0fr\xab\x92i\x83\x8e\u007f\x91 ?i\xeb\xfe<\xa8\xe7\x01\xc2B\x05J\xbf\xeb㫑rxtZL\xc6ڿ\xa8\x8d\n\x16\xd94\xaf\x8e\x8f\xe3sZ\xb1hK(\xfe`\xa3vS\xb3\x06\x04\xb6\x9e\xfb\xbah\xdfW\xb4\xbc\xd6\x13\xb5q\xe7<\xfeD\xd22[\x99\xa5G\x924(>y\x0e\x19\xdfJ\xd3\v3\x0f\xc0+\xe3\xd9\xef$Cz\xab\xf3\x0e}N&7\xc3\xf1\xf4\xe0X\xe7\x1e)J\xab\xb1\xa8\xe4\xcf\xcc\xd4\t\xb4\x11\x8f\xd6\x00\xf3\xf3(\x18p\xb00\a<\xa2\x04%)_F/\xb8\xfck\xf8\xfe\x9c\xe1\xf9\xb5\x05#\xa4\xe3\xaaR(\x96G\xcb\r\xa8\xc5G\xd4\x0f\xe4\x8f\xf4\x11\xf5\x1b3\t\xb12!92B~_\xb3vJ\x17\xcc^C\xce,\xaeF\x00&\xf8\xb1\x11\x95\xa2\xe4\xf1\xc2\xd3M\x1a⭃\xf2Τ\x12B\x84\xc4s\x81ư}|\xb3\xf9\xec\xdc\xd1\x1e\xa5\xdb\xe4F\xb2D!\x14k\x12\x97\xdd\xf7\x8b\xfeD\xc72\xebο\x1e\xb5p\x84m\x8dz3\xb49\xa1\xf6\xee\x84M\x03û\xea\xe0\x9f\xc7\x1d\t\x97\x16\xf7\xd8\r\x8f\xf0\xa5\xe4zٗ\xdf\xd6\xc3\x1cG\xe8\xe8N\x16\xde|f\x00\x05\xdfs\xe7\x10\x9d`\xf7Lo\xd9\x1eW\x99\x12\xee\x1cŕ\xecc\xf4\xcbȕ\x9e\x87\xce\x12r\xe7FԷ<-\x9b\xc7(\x94\xf1\xfdr\xec6`\x05\x9f\xb0\xef\xea}\x82\x1f\xf3\xc7\xfa\x9b\r\x83\x01\x1by\xa7\xd5\xdeEN\x83\x9fn\xa2u\x0f~\xb9c\xdar&\xc4Ƀ\x9fXu\xd0\xfd\x01\x9d}M8\xd4\x11\x06\x1a˴M\xf3]\xf7\x9d\xa1\vn\x8b\xe0b\xbe\x86{,\x993\x92\x812\xd3U\xf5M\xff+\x1eW.\xb4\x8d\x9f\xac\xf0_\x1c\xc8\x0eL\xee\xe9M+h\xa4\xdd\xde?]\x19@\xec\xf8\xa1\x8e\xdf\xe9\xa2\xfe븜\xe6#\x1e\xb7\xcb\xce\xe7\xb17\xb8\x97\u007ftn\xa8\x81\x17]\xc67|7ܖ\xcbR\xf0\xcca\xfb\xed\x17\xca+\x1e\x87\x1f\xf4\x18\x92\x1b>\xe8\x11\xcc2荗C\x04\x90\xeeҺ\xc1\x91yo\xad;(\x0f\x0f\x943\xd1Q3)\xe2d\x95e\x02dUlQ\x93\b\xe2\x80\x01\x03\xe3\xd7N\"\xa8p\a6\x19\x0e%\x13R;\x87s\b\xa9'M\x11b\xaa,Ccv\x95\x10Óo\x1dm\xbc\"U\xcfL\xbb\x10s\xde\x00\xfe\x1a\x06\x8d\xec\xbfa\xfe\xeb\xee\xc0\xad\r8\xe2\xf7+m\xc1#Ql\xaf\xab\xf9\xd8ѻ\xe6?b\xdf*|\xdc\xe8\xe8+)\xc8\xe1\xe5-\xeb\f\xa8\x84\x9e&4fY\x86Nw?\xf5\xbfstyI\xff\xc4O\x19ѿ\x99\x92>\xbfa\xae\xe1o\u007f\xbf\x80p\xc2z\x8cx\xb8\xce\xff\x06\x00\x00\xff\xff\xd1v\x1cU\x18J\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec;\fJ\x96a~\x06p`\x82\xe7D\x9d\xc7I\x95(?\xdc\xdd>\xfe\xef}\xb6ǂ\xf9N\x80\x1cM\xa6yI\xe3\x02r\xc0\r0x$\xd2@\a\x11\x80\xdd3\xeb\xfe#T\xa45`\xf7\b\x19+m\xa5\x11\xd4\x16~\xa86\xa8%Z4\x012@&*cQ\x83\xb1\xcc\"0\v\fJť\x05.\xc1\xf2\x02\xe1\x9b\x0fw\xb7\xa06?cf\r0\x99\x033Fe\x9cY\xcc\xe1\xa0DU\xa0\x9f\xfb\xed:\xc0,\xb5*Q[\x1e\x19\xedZK\xb3\xea\xbe\x1e]\x17\x8ep?\x06r\xa7K\xe8\xd1\x0f\x1a\x819\x18b\x8a\xa3\xc3\xee\xb9\x01\x8d\x81Lb`\v,\xb8!L\x06\xa4\xd7p濫\r\x98\xbd\xaaD\xee\x14\xf0\x80\xda\xf1)S;\xc9\xffYC6`\x15-)\x98\xc5 \xfbظ\xb4\xa8%\x13Nd\x15^\x12#\nv\x04\x8dn\r\xa8d\v\x1a\r1k\xf8\xb3\xd2\b\\n\xd5\x15\xec\xad-\xcdջw;n\xa3-e\xaa(*\xc9\xed\xf1\x1dY\x04\xdfTVi\xf3.\xc7\x03\x8aw\x86\xefVLg{n1s\xc2{\xc7J\xbe\"\xc4%\x99Һ\xc8\xff+J\xdd\\\xb40\xb5G\xa7d\xc6j.wu7\xa9\xfa$ߝ\xce{u\xf2\xd3<\xfe\r{]\x97\xe3\xcaO7\xf7\x0fmU\xe3\xa6\xcbs\xe2v3\xcd4\x8cw\x8c\xe2r\x8b\xda\vn\xabUA\x10Q\xe6^\xd7HM\x05G\xd9e\xba\xa96\x05\xb7N\xd2\xff\xa8\xd08uVk\xb8&\x8f\x02\x1b\x84\xaa̝\x16\xae\xe1V\xc25+P\\3\x83\xbf:\xdb\x1d\x87\xcdʱt\x99\xf1mG\xd8\x1d\xe8\xb9UwG\x975*!o\xf1\xf7%f\x1d\xc3ps\xf8\x96g\xa4\xfe\xb0U\xbaq\b\xde'\xad[\x00nj\xd25|\xc9D\x95c^\xbb\xa5\xde\xef=Tn\x06\xc3ɟ3.\x9d\xfe8\x0f\xealO6\xbf\x92Gb\x1a{@\x01\x9c\f\xb9\xf4\xd0\xc8\xd7\xecq\x04m2=\x8b\xc5\x00\xab\t\x86\aؕ\x10l#\xf0\n\xac\xae\xfaK\xfbyLkv\x1c\xe5D\xdc\xcf\xd2\x18Q\x8f\x0e\x16$xF\x9e\xb6\xb6\x13\xe2\xc5\xef\x88\r{\xa5\x9e\xe6I\xff\x93\x1b\xd1\xd89d\x14\x06\xc0\x06\xf7\xec\xc0\x95\x0e\xc4\x06g\xbbA\xc0\x17\xcc*K\xfb]\xb71\v9\xdfnQ;(\xe5\x9e\x194\u07b9O\xb1`J\x89]\xd3Sb\x1b\xe0߈\x8ci\xf4\xf4N\xa1\f\xcf{\x94\x84̐\xbb\xbe\xb9\x9dX\xe6\xfc\xc0\xf3\x8a\t\xe0\xd2X&3O\a\xabq\xea\xd3\x01\xd3\xe2\x1c`\xeb\x8d?\xe2\xecx\xdfq\x04J\"(\r\x85\xf3yáf\x14>L\x92\xbba\x06sP^\ru%Є\x85r\xf2/\x8d]_N\x00\xae\xa5\xe0wH\xc16(\xc0\xa0\xc0\xcc*=Ɔy\xa1\xfa\xb6\xec\xa3&x7⭂\xd3\f.\xb4\xed\xa8\xd4$L\x80\xe7=\xcf\xf6~\xf3r\xfaBP Wh\xc8~YY\x8a\xe38q0/i\xdffL\xb8i\xb3\xc6܇54\xeb\xa6-\xfa\xb9\xa6-x\xbc./k\xd1\xffqX\x19\x1d\xf7Ɋy;\x98\xf8\x9a\x8a\xe9\x98\xc8]\x00z\xbb\x05,J{\xbc\x04nc\xaf\x8bq\x19\x1d\x9f&\xd9S\xaf\xfd\xbb\x13ĩ:}۟\xf7\x8a:\xfd\v\xa5P/\xfd\xbb\x11\x029\xfb\xfb\xe0\xeb\x13\x05\xf0\xa9=\xe7\x12\xf8\xb6\x16@~\t[.,\xea\x9e$\xe6\xc8U\xf3\x92\xf8\xa5,Xީ\\+\x98\xcd\xf67/.:2M\xd6#\x89\x1b\xfd\xa9>\xa6\x8cQuw3\x9d\x85\ntd\xe2\x1a\v\u007f\x10{ \x0e6=\x14\xf9|\xf8\xfc\x11\xf3i\xa6@\x8a\x86\rH\xf8\xd0C\xb3\xbdl\b\x91\xd3\b\bAJ}\xba\xf0\x87\xeaK`\xf0\x84G\x1f]\xb8#~\x89\x9a\xb9e\xdc\xe0E\x88\x1a\xe9dO\n\xf5\x84G\x02\x12\x0e\xeb\vs\xd3D\xef\xdb\x13\x1e\x97\a\xf5\xd8\xe6\xb0\xe1&$\x1f\x1c\xff\\\a1\x80Nz\xa9,\x03J\xb5D\x0f\xb3D\x14\xa4\xba\x88\xd8\"\xb7O&\xaf\x16S\x93\x1d\xf0\x82\xbc0^(N\xdb\xf7\xbcL\"йN0H6\x11S-\x8fL\xf0\xbc^\xc6\xeb\xf7\xad\xbc\x84\xcf\xca\xdeʩ`\xb5\xdbn^\xb8\t\x19\xae\x8f\n\xcdge\xa9\xe7ՙ\xe8Q>\x99\x85~\x1a\x99\x90\xf4n\xd8\xd1\xdf\xce\xd8,*\xb1o\xb7\xfe\x84U\x8b\x84\x1b\xb8\x95\xee\f\xe1y\xe5sn~\xb19o\xdfmEe(%#\x95\\\xd1f\xb7\x1e['\xb08Q\x91\xdbR\x18\xa2U/\xe9\x97K\x82\xf8\xe0\xf6\x05?\xdb\xe7\x0f\x05\xcb0\x87\xbc\"&R\xfe\x8bY\xdc\xf1\f\nԻ鍠\xddJ\xe7\xb3S\x96O\U000a5f9d\xa4O)[sl\xc1\x19\xe7Kh\xac\x9cm.\x8e\x89\xa2]\x188\x9a\xf0\x9a\x1e\xb8D\am\x92\x147,p\x93\xe59]G0q\x97콓9?ܷ=J~\x8f+X\xe9\xac\xf3_n\xab\"\xa5\xfd7\x94\x8c\xebE\v\xfd@\xf7\n\x02;3CV\xa8\xbd\x88\x83\xcf\r8i\x1e\x98\xe8\xa7MG\xc8R\xcek\xa0\xf0۰\xda\x0e\"\x8dKx\xde+\xe3w\xc5-G\x91\x03\x9f\x8b\xb4\\;\u007f\xc2\xe3\xf9\xe5\xc0\xc6\xcfo\xe5\xb9ߞ\a\x16\x1b\xf7\xf2\x05\xc0J\x8a#\x9c\xd3\xcc\xf3/\x0f]\x92\xb4.a\x10]2\xa5\x05\xb3\xee4\x17wq7\xad\xbe\xa9p\xa1\xe84\xb6\t:W*c\x13\x91\xb8S\xc6\xfa\f]'x\x1c\xc9\r͟iBN\b\xd8\xd6\xdf\x0e)\x1d\xef\x01\x9c#\xeb\xa5*\x9d\x94\f\x8e&8\a\x10\xf3\x00\x92\t\x01獍z\xffx\xee/\ah\t\x96QX0\x03ѩB\xa9U\x86\xc6̩â\xe7]H\xb8\xd5\xc96\xe6\x0f\x15>\xd5>\x97܋-5lt\xac9)̾yi\xe5\x00\x9di\xbb\xff\xe7\xd5\xec4\x8c\x80nj\x8b\x82\xc9\xc5\xcdb\x80ܵ\x9f\x17M!\x80\xf1!\xbb\xdeUdƩ\x91^P\x9a\xaf\xbb\xc1\x16\\\xde\x12px\xff\xaa\xdb1D\x97\x88\xa7\x87\xd4\xd7qf\xc3\xe6\xba\xc3\xdbf\xa9\x86)\xf7\xb1\xf6\xbcG\x8d\x1dI\r3\xc3\x14\xceIe[\xc7\xf34F{<.\fl\xb96\xb6\x8d\xa4\x81j\xd6j[@N:\xa3\xc8\x1b\xad\xbf\xe0\x88\xf2\xa3\x9f\xd7J\x00\xed\xd5s\xbcO\xf3\fI\xa2\x98\xaeA\x10\xf8\x16\xb8\x05\x94\x99\xaa$%1\x9c\x91\xd2\x02\x9e\xa5ޙ.n\xb2\xbe\xa5\x18\xb6k(\xab\"\x85\xf0\x15i\x0f\x973\xb9\x8e\xf6\xe0\xef\x19\x9f\xcbT\xc5v\x92\x98,/PU3\x9bZ\xd3:bz\xf0\xf3:\x17\xa1\x05{\xe1EU\x00+\x1c\xb3\x938\xeavf^`W\xbe\xf0̸%\xef\ue812\xab\xb7\xca\x19E)Ц\x9d\x066\xb8U\x9al\xd1\xf0\x1c\xeb-3\xc8\\I`\xb0e\\T:ɣ\x9d\xc0\xd1\xf4\xc8>\x18\xf9\xeb\x04\xed)ˮ\x88\xfc\xc54eR\xa86\xe7UK\x9d\x1a\xa8\xddi|\xcd\x10\xa9\xd4\xdc\xe9\x8cz\xdd()\xa8\x12\x93Ƿ0\xa9ś\xb70i\xd0\xde¤N{\v\x93\xde¤\xd9\xf6\x16&\xbd\x85I\u007f\xd40i\x1e\x93\x15\xe5\xadF\u007fZX}\xf1\nu\x1a\xb1I\xc8\xe1V\xff\xdaW(\xa7\xd5\xe5ݎ\xcfi\xf9\xab\xe7=\xda=\xeaX\xf8\xbc\xa2\xba졜\x9b\xab\xff\xc6\xcdׅzN\xf9\xa3\xf2\xfa\xf2\xcb\xd9ҽ\x84B\xbc\x8dR\x02\x99\x1c\xa3?\xb56s\xa4\xa8\xa4[\x93X\x17vĢD\x15\x97\x18P\x1f\xeb\xbd]\x98ٮ``B\xb4kS\x98n\x98\xf2\x95\xea\x15\x17K?\x16\n>\xe6\xcb6\xa79\xd4\v\xed\xbb,ҝ\x12ï̡ٺ\x8c\xe9j\x8cp\x93\x81\x96\x1dޯ\xbb\xbfX\x15j3\xe0\x99\xdb\xfd\x80\x00*\x9atG\x16\xb9k\x17GF\x9d\nE\xf6}\u0381\xd2 \xb9\xb8\x1c\xad\x8b\xa9\xdf\x1f\xb4\xd9\t?\x96\xfePt\x92\xbdͅ\xf6)\xb5\x1b_\\\xb1ѭ\xc9\x18u\xb2\xa7]v\xa4\x96\x90\xa6\xd7dtk.&6\x99\x84J\x8c\x93+-\x96\xcf[\xb3U\x15_PK\x11\xeb$\xe66ܙ\n\x8a\x84\x98c\xb9Z\xe2\x8bj$\xe82o\x06\xeb\x93*#ZU\x0f3 \xd3\xea!\x12X\xb2T\xfbpr\xc5C\xbf\xca`\x86\x88\xa5:\x87\xe9\x1a\x86\x19\xa0\xa3\xd5\r)\x95\v30뚆W\xacWX\xa8Rx\x9dJ\xc2_\x1a{N\xd5\x1c,T\x1a,D\xa6sX-\xd4\x12\xa4W\x10,\xf0\xe7\v\xab\x05\xeaz\x80\xd15O\xad\x11\xe8V\x01\x8c\x82L\xac\f\x98\xb8\xfb\x1f\x05\x99P\x0f\xb0p\xe3?\nvvc\x9cшɟ\x8cd\xa5\xd9+\xfbHO\x1a\xe7\xa3\xc8\xfb\xeeؑÅ\x8bq\xd8\x13B&T\x95װ\x87\xa4\xd0C\xc5#\xdc=\x92\x93\xa7\xa70Y\xf3\x10(\xb8\xf2\x18\xfc\xf4\xdf\t}\xf7\x9a\x87\rc\x95f;\xfc\xa4\xb2\xd6{\xd4)\xfa\xbbc;\x8f\a\x83P\xe3\x91>\xd6A\xb0\xf8\x8a\xad;u,v\fY6\x1f\x1f\xb6N_\x0eá\xbc'-\xcfZ1K\xc4\xc3\xc3'\x8f\xb8\xe5\x05\xae?V\xfe \xb7*\x996\xe8\xf8\x17\t\xf2\x936\xeeϽz\x1e ,T\xa0\xf4\xbb>\xbe\x1a)\x87G\xa7\xc5d\xac\xfd\x8bڨ`\x91M\xf3\xea\xf88>\xa7\x15\x8b\xb6\x84\xe2\x0f6j;5k@`빯\x8b\xf6}E\xcbk=Q\x1bw\xce\xe3O$-\xb3\x95Yz$I\x83\xe2\x93\xe7\x90\xf1\xad4\xbd0\xf3\x00\xbc2\x9e\xfcN2\xa4\xb7:\xef\xd0\xe7dr=\x1cO\x0f\x8eu\ue462\xb4\x1a\x8bJ\xfe\xccL\x9d@\x1b\xf1h\r0?\x8f\x82\x01\a\vs\xc0\x03JP\x92\xf2e\xf4\x82˿\x86\xef\xcf\x19\x9e_[0B:\xae*\x85by\xb4܀Z|D\xfd@\xfeH\x1fP_\x98I\x88\x95\tɑ\x11\xf2\xfb\x9a\xb5U\xba`\xf6\nrfq5\x020\xc1\x8f\x8d\xa8\x14%\x8f\x17\x9en\xd2\x10o\x1d\x94w&\x95\x10\"$\x9e\v4\x86\xed\xe2\x9b\xcdg\xe7\x8ev(\xdd&7\x92%\n\xa1X\x93\xb8\xec\xbe_\xf4':\x96Yw\xfe\xf5\xa8\x85#lk\xd4\xc5\xd0\xe6\x84ڹ\x136\r\f廙\u007f\x1ew$\\Z\xdca7<\u0097\x92\xebe_~S\x0fs\x1c\xa1\xa3;Yx\xf3\x99\x01\x14|ǝCt\x82\xdd1\xbda;\\eJ\xb8s\x14W\xb2\x8fѯ#W\x0fu\xe4\x1b\x02\x03\x82\xbeo\x8f\x8c\a\xc1\xa0\xcc\x1eJ\xfc\xa4\xc0e\xd8Q\x9d\x04\v\xf6\xb3\xd2\xc3˚\x82K\xa5}\xf8B!t\x9c\x9a\xec\xcf\xe9Y\xeb,\xbewnD};\xd5\xf2U\x18\x95i|\x9f\x1f\xbb\xc5X\xc1g\xecoQ\xfeb\x02\xf3\xc7\xfa[\x13\x83\x01\xb7\xf2N\xab\x9d\x8b\xf8\x06?]G\xaf4\xf8\xe5\x8ei˙\x10G\x0f~b\xd5A\xf7Gt~ab#\x18a\xa0\xb1L\xdb4\x9f{\xdf\x19\xba\xe0n\t.\xe6k\xb8ǒ9\xe3\x1eȞ\xaeد\xfb_\x1f\xb9t!y\xfcԆ\xffRB\xb6grGoqA#E)\xfe\xc9\xcd\x00b\xc7\u007fv\xfce\x17\xf5\xdf\xc6U6\x1f\x1f\xb9Yv\x9a\x8f\xbd\xc1\xbd\xbc\xa9s\x9f\r\xbc\xe8\xea\xbe\xe1\xdba8Q\x96\x82g\x0e\xdbo\xbfR>\xf4\x90\xe0D.f\xfd\a9\x8b\xda\x15\xc0G,5fn\u007f\x18\"\u007f'Й\xb7A\xec:\xa6\x8bd?ލ\b\xcd\ak\xb1(G֚\t\t\x9bI\x91\x1e\xab,\x13 \xabb\x83\x9a\xe4\x17\a\f\b\x88\x9fx\x89\xa0\xc2\xc5\xdfd\f\x98LH\xedYN!\xa4\x9e4E\x88\xa9\xb2\f\x8d\xd9VB\f\x8f\xfbu\x88\xf5\x8aT=3\xed\xe2\xeay\xeb\xf9k\x184\x12t\x84\xf9\xaf\x1bv\xb4\xa2\x8e\x88\xdfo\x14w\x8c\x84\uef6e\xe6\vO\xef\x9b\xff\x88}\xab\xf0E\xa7\x83/\x1f!o\x99\xb7L;\xa0\x12z\x9a\xf3\x00\xcb2t\xba\xfb\xb9\xffq\xa7\xf3s\xfa'~\xbf\x89\xfe͔\xf4I\x1ds\x05\u007f\xfb\xfb\x19\x84c\xe5c\xc4\xc3u\xfe'\x00\x00\xff\xff\x03\xba\x12'\rK\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcX\xcdr\xdb6\x10\xbe\xeb)v܃/\x15\x95\xa4=txs\x94v&S'\xf1X\x89{h;\x13\bXJ\xa8A\x80\xc5\x02rԧ\xef,\bR\"E\xd9\xceLS\xde\b,\x17\xdf~\xfb\v\xce\xe6\xf3\xf9L4\xfa\x0e=igK\x10\x8d\xc6/\x01-\xbfQq\xff\x13\x15\xda-v/\xd7\x18\xc4\xcbٽ\xb6\xaa\x84e\xa4\xe0\xea[$\x17\xbd\xc47Xi\xab\x83vvVc\x10J\x04Q\xce\x00\x84\xb5.\b^&~\x05\x90\xce\x06\xef\x8cA?ߠ-\xee\xe3\x1a\xd7Q\x1b\x85>\x9dН\xbf{Q\xbc*~\x9c\x01H\x8f\xe9\xf3\x8f\xbaF\n\xa2nJ\xb0ј\x19\x80\x155\x96\xb0\x16\xf2>6\x14\x9c\x17\x1b4N\xb6g\x15;4\xe8]\xa1\u074c\x1a\x94|\xf4ƻؔp\xd8h5dX\xadI\xaf\x93\xb2U\xab\xec:+K\xfbFS\xf8\xf5\xbc̵\xa6\x90\xe4\x1a\x13\xbd0\xe7`%\x11\xd2v\x13\x8d\xf0g\x84f\x00\x8dGB\xbf\xc3O\xf6\u07ba\a\xfb\x8bF\xa3\xa8\x84J\x18\xc2\x19\x00I\xd7`\t\xef\x19}#$\xaa\x19\xc0N\x18\xad\xd2\xf7\xad=\xaeA{u\xf3\xf6\ue1d5\xdcb-\xdaE\x00\x85$\xbdn\x92ܴ%\xa0\t\x04t`\xe0a\x8b\x1e\xe1.\x91\x06\x8c\x14)\xc3\xce\x1a\x01\xdc\xfa/\x94\x81\x8a\xbc\xd0xנ\x0f\xbac\x96\x9f\xa3\xc8\xea\xd7F`.\x19m+\x03\x8ac\t\t\xc2\x16!G\x04*\xa0d\t\xb8\n\xc2V\x13xL4\xd9ppR\x8f\xa8\x02a3\xae\x02VL\xa5'\xa0\xad\x8bFq\x00\xee\xd0\a\xf0(\xdd\xc6\xea\u007fz\xcd\x04\xc1\xa5#\x8d\b\x98\xdd\xd9=\xda\x06\xf4V\x18\xe69\xe2\xf7 \xac\x82Z\xec\xc1#\x9f\x01\xd1\x1eiK\"T\xc0;\xe7\x11\xb4\xad\\\t\xdb\x10\x1a*\x17\x8b\x8d\x0e].IW\xd7\xd1\xea\xb0_\xa4\x8c\xd0\xeb\x18\x9c\xa7\x85\xc2\x1d\x9a\x05\xe9\xcd\\x\xb9\xd5\x01e\x88\x1e\x17\xa2\xd1\xf3\x04ܶ\xe1]\xab\xef|N<\xba\xa3\x03\xf1L\x94\xb6\x15\xfa\xd6q\x95wu҈V5Nې^\xa4\xd1h\x87\xa4S\\\xd7:\xb0\xa7\xff\x8eH\x81\xfdS\xc02U\x14X#\xc4F\x89\x80\xaa\x80\xb7\x16\x96\xa2F\xb3\x14\x84ߜvf\x98\xe6L\xe9\xd3\xc4\x1f\x17¡`\xcbV\xbf\xdcըI\x0fM\xa6\xe9\xaaA9\xc8\x13V\xa1+\x9dӶr\x1eDN\xdb\x01\xa7\x93ʊ#\x91\xa9\xf4M),%\x12\xbds\n\x87\xeb#\xb0W\xbd\xd8\x00]\x83\xbe֔\x9aI\xc2\xc6km\x19\x81\\\xfeFJ\xa1/A\xc5h\am\xac\xc7\x10\xe6p\x8bB}\xb0f?\xb9\xf1\x9b\xd7a|\xc0\xa4\xc3\xf8ia\xad\xf6Vޠ\xd7N=j\xee\xeb\x91po\xf4\xd6=@\x95\x02\xd7\x06\xb3\xe7\xcaB{+ǥ\xb3{\xaen\xdeve\xb4M\x8f\x9cM\x99\x9b\x02\xaerV\xba\n^\x80\xd2$\xd6\x06)\xa9\x1c\xd3\xc3͑wK\b>>\xdbh\xe9l\xa57cS\x85R\xa9\xa3\vss&*\x1eU:\xe2j\x99\xce\xe0R\xc3\x11\xd0x\xb7\xd3\n\xfd\xbc\v܌!\xfa\x1c\xc1\xa9鍭\x9b\xcc\x1e\xe8\xcbO\x8e\xebG]\xf6\xe1X\xb2\xefz\x19E\x97L\x18\xb8\xee\x11X\xe4p\x16~\x1cW\xc0\x1e\x95\xceZ\xf6Rp z{.i\xec\xbcѧ\xe7\x12\x8c\x9fu\x94\xf7\x18N\xd7\xc7Q\x97ĘɔG\xed[p\x10\t\x13\xb7\x8f\x03x\xc2g\x00R,\xd1?\x8dby\xc5b}\xc4\vX^\xc1:Ze\xb0\xc3\xf2\xb0E\xcb\r\\W{\xee\"\x1f\xafW\x13:\xa1\xe31\x15\x87܂;6\xa7\xb0W\xce\xd7\"\x94\xb0ޟ$\xf5\x93\xa65\x1e+\xfd\xe5I\xd3n\x92XGp#\xc2\x16\xb4%\xad\x10\xc4\x04\xdd\x13e\xb6{\xfa\x04\xfeд\x89\xf4\x95\xce\xe0\n\xa2=\x9e\x14\xa1y\x86\xf1\xdc\xf4\xe8\xf8|43n\xb2Pow\xf7\x9eF\xaeq\xc1\x9eN\xcd\x13+\xa6,\x98\x0f\xd3u\xb0\xd3\x1d\xfad\xcb\f\"D\xfaʦ\x99\xbeɂ\xeb\x9c\xeb2z\x8f6d\x85\xe0\xaaa\xdb\xebf\xdfo\xdd8/\x8e:'Oc\x16\xa2\x8d\x84\xaa-\x84\x05\xfca\xe1\r\xcfV\x92g\x9e\x92\x91\xf3\x98C'\xe1d\xdd\x03\u007f|\xa4-)\x00g\x93\xb5in\xe0\xe9\xb5\x1d\xc5\xd2փ6\x86\a*\x8f\xb5ۥ\xdb\xc4\xf0\xe1\xe9ǣك`z`\xf7\xaaxQ\\\xfc\xcf]\xd9\b\n\xdcfQ\xdd\xe2N\x8fo\x12\xa7l^\x9f\xc8wQ\xdd7R~\xf9܍h\v\x9f\xc5>\x9f\x98_i\xc3\x03\xe5D\n\x1c\xaeI\xed\xb5\x81\x02\x04]cz{\xbd\xba\xbe\xa4t\xdb\xe5a\xf8D\xe9\x03\xbb\x8f\x12@\xbe\\\xb8<\x03G\n\xe8'\x9c\xdd\xfbJ\x13X\a\xc6\xd9\xcd E\xda'O\xc4\xe0<\xb4\xa1\xe3<(\xe4a\x96˯\xdc\n\xbb\xc1\xc3-'c?BɁq\x8at\x18\x1d\x87h\xd0v:\x14\x9e\xe1C\xbe\xcc?\xea\xbf\xeb\x81h\xe7\xba!\xc3=\xea\xecKs|_\u007f6\xd7#鮹0\x91s>\xea?\x19\xaf\x9a\xad\xa0\xc7\r\xbea\x89\xce\xce\xe3\x92ԇ\xea\x93\x05\bΦ\xe1\xd5N\xe8\x84\xfad\xe7\x93\x15g\xf6\xce\xd82Q\x8bGK\x87_8/\x0fo\xa9&\xce\xf3/\x9b\xb4\x01\x90~q\xa8#\"sV\xe5\x95C\x81\xe7\n\xda\x04T\xefǿk..\x06\xff\\ҫt\xb6\x1dZ\xa9\x84\xdf\xff\x9c\xb5ZQ\xddu8x\xf1\xdf\x00\x00\x00\xff\xff\xc1e\xcb@\xee\x12\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96\xcdr$5\f\x80\xef\xfd\x14\xaa\xe5\xb0\x17\xa6\x87\x00\a\xaao\x90]\xaaR@*\x95,\xb9P\x1c<\xb6fZ\xc4m\x1bI\x9e\x10\x9e\x9e\xb2\xbb;\xf3\x93Ne9l\xdf,\xcb\xfa\xf9$\xb9ݬV\xab\xc6$\xbaG\x16\x8a\xa1\x03\x93\b\xffQ\fe%\xed\xc3\x0f\xd2R\\\xef/6\xa8\xe6\xa2y\xa0\xe0:\xb8̢q\xb8E\x89\x99-~\xc0-\x05R\x8a\xa1\x19P\x8d3j\xba\x06\xc0\x84\x10\xd5\x14\xb1\x94%\x80\x8dA9z\x8f\xbc\xdaah\x1f\xf2\x067\x99\xbcC\xae\x1ef\xff\xfbo\xdao\xdb\xef\x1b\x00\xcbX\x8f\u007f\xa2\x01E͐:\b\xd9\xfb\x06 \x98\x01;p\xe8Qqc\xecCN\x8c\u007fg\x14\x95v\x8f\x1e9\xb6\x14\x1bIh\x8b\xe3\x1dǜ:8l\x8c秠Ƅ>TS?US\xb7\xa3\xa9\xba\xebI\xf4\x97\xd74~\xa5I+\xf9\xcc\xc6/\aT\x15\x84\xc2.{Ë*\r@b\x14\xe4=\xfe\x1e\x1eB|\f?\x13z'\x1dl\x8d\x17l\x00\xc4Ƅ\x1d\\\x97\xa8\x93\xb1\xe8\x1a\x80\xbd\xf1\xe4*\x9e1\x8f\x980\xfcxsu\xffݝ\xedq0\xa3\x10\xc0\xa1X\xa6T\xf5\x96r\x00\x1200E\x02\x1a\xa7\x00!\x06\x84\xc80DF\x18\xa3\x95v2\x998&d\xa5\x99`\xf9\x8e\xfa\xe7Yv\xe6\xfc}\x89n\xd4\x01W:\x06\x05\xb4G\x98\xea\x8e\x0e\xa4F\x0eq\vړ\x00c\xc5\x12\xc6\x1e:2\vE\xc5\x04\x88\x9b\xbf\xd0j\vw\x05\x1d\vH\x1f\xb3w\xa5\xcd\xf6\xc8\n\x8c6\xee\x02\xfd\xfblYJ~ť7:\x17x\xfe((r0\xbep\xcd\xf85\x98\xe0`0O\xc0X|@\x0eG֪\x8a\xb4\xf0[\x81Ca\x1b;\xe8U\x93t\xeb\xf5\x8et\x9e\x18\x1b\x87!\aҧu\xed{\xdad\x8d,k\x87{\xf4k\xa1\xddʰ\xedI\xd1jf\\\x9bD\xab\x1ax\xa8\x03\xd3\x0e\xee+\x9e\xc6K\xde\x1fE\xaaO\xa5\x13D\x99\xc2\xeeY\\{\xf8U\xee\xa5\u007f\xc72\x8f\xc7\xc6\xf8\x0fx\x8b\xa8P\xb9\xfdx\xf7\tf\xa7\xb5\x04\xa7\xcc+\xed\xc319\x80/\xa0(l\x91\xc7\xc2m9\x0e\xd5\"\x06\x97\"\x05\xad\v\xeb\t\xc3)tɛ\x81T\xe6\xf6+\xf5i\xe1\xb2\xde\x1b\xb0A\xc8\xc9\x19E\xd7\xc2U\x80K3\xa0\xbf4\x82_\x1c{!,\xab\x82\xf4m\xf0\xc7\xd7ݩ\xe2H\xebY<\xdfE\x8b\x15Z\x18˻\x84\xb6Ԭ\x80+giK\xb6\x8e\x01l#\xc3cO\xb6\x9f\xc7\xf2\x84\xe8\xf3\x00\xb7G⥁-\xdfh\xa0\xdc*\xa7\xf2W\x92\x85Z'b<\xe9\xb5Ց\x997)\xa8\xd1,\xff\x8bC=1\x93\xb0\x99\x19\x83Nv\xea-\xb0t\xe8srG\xe6\xc8r\x9e\xf7I8\x1f\xabJ\xfdk\x19\n\x02&t\xb1\xb1\x16\x93\xa2\xbb>\u007fN\xbc{w\xf2.\xa8K\x1b\x83\xa3\xf15\x04\u007f\xfcٌV\xd1\xdd\xcfq\x14\xe1\u007f\x01\x00\x00\xff\xff\xcb0\x9b\f\x8c\t\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WOoܶ\x13\xbd\xebS\f\xf2;\xe4W \xd26m\x0f\x85n\xad\x93\x02A\xd3 \xb0\x13_\x8a\x1e\xb8\xe4\xacĚ\"Y\xcep\x1d\xf7\xd3\x17CI\xfbG+\xdb\xe9\xa1{\xd3p8|||oȭ꺮T\xb4\xb7\x98\xc8\x06߂\x8a\x16\xbf0z\xf9\xa2\xe6\xeeGjl\xd8\xec_o\x91\xd5\xeb\xea\xcez\xd3\xc2U&\x0e\xc35R\xc8I\xe3\x1b\xdcYo\xd9\x06_\r\xc8\xca(Vm\x05\xa0\xbc\x0f\xac$L\xf2\t\xa0\x83\xe7\x14\x9c\xc3Tw蛻\xbc\xc5m\xb6\xce`*+\xcc\xeb\xef\xbfm\xbek~\xa8\x00t\xc22\xfd\x93\x1d\x90X\r\xb1\x05\x9f\x9d\xab\x00\xbc\x1a\xb0\x05\x13\xee\xbd\v\xca$\xfc+#15{t\x98BcCE\x11\xb5,ڥ\x90c\vǁq\xee\x04h\xdc̛\xa9\xcc\xf5X\xa6\x8c8K\xfc\xeb\xda\xe8{;eD\x97\x93r\x97 \xca Y\xdfe\xa7\xd2\xc5p\x05\x10\x13\x12\xa6=~\xf6w>\xdc\xfb_,:C-\xec\x94#\xac\x00H\x87\x88-|\x10\x94Qi4\x15\xc0^9k\n\x15#\xee\x10\xd1\xff\xf4\xf1\xdd\xed\xf77\xba\xc7A\x8dA\x00\x83\xa4\x93\x8d%o\x89\x1b,\x81\x82\t\x05p8\x00\x03\xe5A%\xb6;\xa5\x19v)\f\xb0U\xfa.ǩ&@\xd8\xfe\x89\x9a\x818$\xd5\xe1+\xa0\xac{PRmL\x04\x17:\xd8Y\x87\xcd4%\xa6\x101\xb1\x9dY\x96߉\xbe\x0e\xb1\x05\xe0\x97\xb2\xa31\a\x8c(\n\t\xb8G\x98t\x81\x06\xa8\xec\x16\xc2\x0e\xb8\xb7\x04\t\v\x95~\xd4\xd8IY\x90\x14\xe5'\xe4\r\xdc\b݉\x80\xfa\x90\x9d\x11\x19\xee11$ԡ\xf3\xf6\xefCe\x12^dI\xa7x\x16\xc2\xfc\xb3\x9e1y\xe5\xe4,2\xbe\x02\xe5\r\f\xea\x01\x12\x16v\xb2?\xa9VR\xa8\x81\xdfBB\xb0~\x17Z\xe8\x99#\xb5\x9bMgyv\x94\x0eÐ\xbd\xe5\x87M\xf1\x85\xddf\x0e\x896\x06\xf7\xe86d\xbbZ%\xdd[F\xcd9\xe1FE[\x17\xe0\xbe\x18\xaa\x19\xcc\xff\xd2d?zy\x82\x94\x1fD=\xc4\xc9\xfa\xee\x10.:\u007f\x94w\xd1\xf9(\x8fqڈ\xffH\xaf\x84\x84\x95\xeb\xb77\x9f`^\xb4\x1c\xc19\xe7\xa3N\x0e\xd3\xe8H\xbc\x10e\xfd\x0e\xd3xpEeR\x11\xbd\x89\xc1z.\x1f\xdaY\xf4\xe7\xa4S\xde\x0e\x96i\x96\xad\x9cO\x03W\xa5\xaf\xc0\x16!G\xa3\x18M\x03\xef<\\\xa9\x01ݕ\"\xfc\xcfi\x17\x86\xa9\x16J\x9f'\xfe\xb4\x1d\x9e'\x8el\x1d\xc2s\xbfZ=\xa1\x85\x95o\"j9/!M\xe6ٝ\xd5\xc5\x02\xb0\v\t\xd4\xd1\xd9\x13m\xcdI\xdd5o\x16P*u\xc8\xe7\xb1\x05\x8aO%E\x16\xbe\xef\xd5y\v\xf9?6]#}\x80&\bcg\xf8\xa6Y\xd4{l\xf55\x8d\xaeb\x98\xa5*[\x17\x1e\xc5\xe8\xd2zN\xd1,\x17\x95\x1f\xfa<\xac\x15\xaf\xe1\xe7\x82\xf4}\xe8\x9e\x18\xbd\n\x9eE\xd0O\xa4\xdc\x06\x97\a\xbc\xf1*R\x1f\x9e̜/\xcd\xc3E\xb2L\xbbFi\xb5\xf8\x18\xa4i\xf8\x1a)\xbbՅV\x858\xff\xca\xc5\xf9\x1c\xcbr\xf7\xcc,˄\xb1\xe3\"ȅ\x9d<2ұ\r\xdc[\xeeᾷ\xba_\xa9\neZ9 \xe9/DA\xdb\xe2\xd8\u007f\a[tl\x13^ȣ.\xa2\xb9\b\n\xe4j\xad\xf8\xc2s\xeb\x85\xeb\xc9\v\xcf:\x96\x15g\xfajϖ\xec\x99T\x9dSB\xcfS\x8dr[-'|\x8dig\xc5\u007f\xbe~\xff\xa4s\xdf\x1c\xf3\xca\x1bLY?\xe2\x88\tk\xb2\x9dܭ2&\xde-\xceZ\x120\xfeN\xef\xf8gO\r\xbfD\x9bN\x9e,\x8f@{{H\x1b\x1b\v\xfa\xf1\x8aX\xbe^J9\xa4r\xedj\xe5/\xb0m\x11\f:d4\xb0}\x18;\xe3\x031\x0eK\xbc\xbb\x90\x06\xc5-\xc8\xc5Q\xb3\xbd\x10\x8a\xbc/\xd5\xd6a\v\x9c\xf2\xba\x8aV6\x1b{E\x17\xb6:\xdb\xe7G\xc9X;\xfe\x83\xb9\x9e8\u007fx\xa4\x83\xd5\xf0\x01\xef/b\x1fS\xd0H\x84Kc<\x82~E܋\xd0\xf1a\xfe\xfa\xf8U\xa4XO\x0f\xf12\x00P\x9e\xb5愺\xe9\xcd8E\x8e\x8eQZcd4\x1f\x96O\xf1\x17/\xce\xde\xd6\xe5S\ao\xec\xf8/\x02~\xff\xa3\x1a\xab\xa2\xb9\x9dqH\xf0\x9f\x00\x00\x00\xff\xff\xbbظ3\xc4\f\x00\x00"), diff --git a/pkg/generated/crds/manifests/velero.io_backups.yaml b/pkg/generated/crds/manifests/velero.io_backups.yaml index f03064cf1..2b0b34606 100644 --- a/pkg/generated/crds/manifests/velero.io_backups.yaml +++ b/pkg/generated/crds/manifests/velero.io_backups.yaml @@ -340,6 +340,10 @@ spec: format: date-time nullable: true type: string + formatVersion: + description: FormatVersion is the backup format version, including major, + minor, and patch version. + type: string phase: description: Phase is the current state of the Backup. enum: @@ -366,7 +370,8 @@ spec: nullable: true type: array version: - description: Version is the backup format version. + description: 'Version is the backup format major version. Deprecated: + Please see FormatVersion' type: integer volumeSnapshotsAttempted: description: VolumeSnapshotsAttempted is the total number of attempted diff --git a/pkg/test/fake_discovery_helper.go b/pkg/test/fake_discovery_helper.go index 2b94b86dc..4d095e083 100644 --- a/pkg/test/fake_discovery_helper.go +++ b/pkg/test/fake_discovery_helper.go @@ -145,6 +145,7 @@ func (dh *FakeDiscoveryHelper) APIGroups() []metav1.APIGroup { type FakeServerResourcesInterface struct { ResourceList []*metav1.APIResourceList + ApiGroup []*metav1.APIGroup FailedGroups map[schema.GroupVersion]error ReturnError error } @@ -159,9 +160,20 @@ func (di *FakeServerResourcesInterface) ServerPreferredResources() ([]*metav1.AP return di.ResourceList, &discovery.ErrGroupDiscoveryFailed{Groups: di.FailedGroups} } -func NewFakeServerResourcesInterface(resourceList []*metav1.APIResourceList, failedGroups map[schema.GroupVersion]error, returnError error) *FakeServerResourcesInterface { +func (di *FakeServerResourcesInterface) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + if di.ReturnError != nil { + return di.ApiGroup, di.ResourceList, di.ReturnError + } + if di.FailedGroups == nil || len(di.FailedGroups) == 0 { + return di.ApiGroup, di.ResourceList, nil + } + return di.ApiGroup, di.ResourceList, &discovery.ErrGroupDiscoveryFailed{Groups: di.FailedGroups} +} + +func NewFakeServerResourcesInterface(resourceList []*metav1.APIResourceList, apiGroup []*metav1.APIGroup, failedGroups map[schema.GroupVersion]error, returnError error) *FakeServerResourcesInterface { helper := &FakeServerResourcesInterface{ ResourceList: resourceList, + ApiGroup: apiGroup, FailedGroups: failedGroups, ReturnError: returnError, } diff --git a/site/docs/master/output-file-format.md b/site/docs/master/output-file-format.md index 03b12d0a4..887743451 100644 --- a/site/docs/master/output-file-format.md +++ b/site/docs/master/output-file-format.md @@ -42,6 +42,7 @@ rootBucket/ }, "status": { "version": 1, + "formatVersion": "1.1.0", "expiration": "2017-08-01T13:39:15Z", "phase": "Completed", "volumeBackups": { @@ -57,7 +58,129 @@ rootBucket/ ``` Note that this file includes detailed info about your volume snapshots in the `status.volumeBackups` field, which can be helpful if you want to manually check them in your cloud provider GUI. -## file format version: 1 +## Output File Format Versioning + +The Velero output file format is intended to be relatively stable, but may change over time in order to support new features. + +In order to accommodate this, Velero follows [Semantic Versioning](http://semver.org/) for the file format version. + +Minor and patch versions will indicate backwards-compatible changes that previous versions of Velero can restore, including new directories or files. + +A major version would indicate that a version of Velero older than the version that created the backup could not restore it, usually because of moved or renamed directories or files. + +Major versions of the file format will be incremented with major version releases of Velero. +However, a major version release of Velero does not necessarily mean that the backup format version changed - Velero 3.0 could still use backup file format 2.0, as an example. + +## Versions + +### File Format Version: 1.1 (Current) + +In version 1.1, we have added the support of API groups versions as part of the backup (previously, only the preferred version of each API Groups was backed up). Each resource has one or more sub-directories, one sub-directory for each supported version of the API group. The preferred version API Group of each resource has the suffix "-preferredversion" as part of the sub-directory name. For backward compatibility, we kept the classic directory structure without the API Group version, which sits on the same level as the API Group sub-directory versions. +By default, only the preferred API group of each resource is backed up. +In order to take a backup of all API group versions, you need to run the Velero server with `--features=EnableAPIGroupVersions` feature flag. This is an experimental flag and the restore logic to handle multiple API Group Versions will be added in the future. + + +When unzipped, a typical backup directory (e.g. `backup1234.tar.gz`) taken with this file format version looks like the following (with the feature flag): + +``` +resources/ + persistentvolumes/ + cluster/ + pv01.json + ... + v1-preferredversion/ + cluster/ + pv01.json + ... + configmaps/ + namespaces/ + namespace1/ + myconfigmap.json + ... + namespace2/ + ... + v1-preferredversion/ + namespaces/ + namespace1/ + myconfigmap.json + ... + namespace2/ + ... + pods/ + namespaces/ + namespace1/ + mypod.json + ... + namespace2/ + ... + v1-preferredversion/ + namespaces/ + namespace1/ + mypod.json + ... + namespace2/ + ... + jobs.batch/ + namespaces/ + namespace1/ + awesome-job.json + ... + namespace2/ + ... + v1-preferredversion/ + namespaces/ + namespace1/ + awesome-job.json + ... + namespace2/ + ... + deployments/ + namespaces/ + namespace1/ + cool-deployment.json + ... + namespace2/ + ... + v1-preferredversion/ + namespaces/ + namespace1/ + cool-deployment.json + ... + namespace2/ + ... + horizontalpodautoscalers.autoscaling/ + namespaces/ + namespace1/ + hpa-to-the-rescue.json + ... + namespace2/ + ... + v1-preferredversion/ + namespaces/ + namespace1/ + hpa-to-the-rescue.json + ... + namespace2/ + ... + v2beta1/ + namespaces/ + namespace1/ + hpa-to-the-rescue.json + ... + namespace2/ + ... + v2beta2/ + namespaces/ + namespace1/ + hpa-to-the-rescue.json + ... + namespace2/ + ... + + ... +``` + +### File Format Version: 1 When unzipped, a typical backup directory (e.g. `backup1234.tar.gz`) looks like the following: @@ -97,17 +220,3 @@ resources/ ... ... ``` - -## Output File Format Versioning - -The Velero output file format is intended to be relatively stable, but may change over time in order to support new features. - -In order to accommodate this, Velero follows [Semantic Versioning](http://semver.org/) for the file format version. - -Minor and patch versions will indicate backwards-compatible changes that previous versions of Velero can restore, including new directories or files. - -A major version would indicate that a version of Velero older than the version that created the backup could not restore it, usually because of moved or renamed directories or files. - -Major versions of the file format will be incremented with major version releases of Velero. -However, a major version release of Velero does not necessarily mean that the backup format version changed - Velero 3.0 could still use backup file format 2.0, as an example. -